Software RAID: Add UEFI support

The proposed changes concern two steps:

First, when creating the RAID configuration, have a GPT partition
table type (this is not necessary, but more natural with UEFI).
Also, leave some space, either for the EFI partitions or the BIOS
boot partitions, outside the Software RAID.

Secondly, when installing the bootloader, make sure the correct
boot partitions are created or relocated.

Change-Id: Icf0a76b0de89e7a8494363ec91b2f1afda4faa3b
Story: #2006379
Task: #37635
This commit is contained in:
Raphael Glon 2019-08-06 13:52:13 +02:00 committed by Arne Wiebalck
parent d71a8375fa
commit 9343348106
7 changed files with 1277 additions and 107 deletions

View File

@ -20,6 +20,7 @@ import shutil
import stat
import tempfile
from ironic_lib import utils as ilib_utils
from oslo_concurrency import processutils
from oslo_log import log
@ -86,6 +87,7 @@ def _get_partition(device, uuid):
# NOTE(TheJulia): We may want to consider moving towards using
# findfs in the future, if we're comfortable with the execution
# and interaction. There is value in either way though.
# NOTE(rg): alternative: blkid -l -t UUID=/PARTUUID=
try:
findfs, stderr = utils.execute('findfs', 'UUID=%s' % uuid)
return findfs.strip()
@ -347,14 +349,127 @@ def _manage_uefi(device, efi_system_part_uuid=None):
shutil.rmtree(local_path)
# TODO(rg): handle PreP boot parts relocation as well
def _prepare_boot_partitions_for_softraid(device, holders, efi_part,
target_boot_mode):
"""Prepare boot partitions when relevant.
Create either efi partitions or bios boot partitions for softraid,
according to both target boot mode and disk holders partition table types.
:param device: the softraid device path
:param holders: the softraid drive members
:param efi_part: when relevant the efi partition coming from the image
deployed on softraid device, can be/is often None
:param target_boot_mode: target boot mode can be bios/uefi/None
or anything else for unspecified
:returns: the efi partition paths on softraid disk holders when target
boot mode is uefi, empty list otherwise.
"""
efi_partitions = []
# Actually any fat partition could be a candidate. Let's assume the
# partition also has the esp flag
if target_boot_mode == 'uefi':
if not efi_part:
LOG.debug("No explicit EFI partition provided. Scanning for any "
"EFI partition located on software RAID device %s to "
"be relocated",
device)
# NOTE: for whole disk images, no efi part uuid will be provided.
# Let's try to scan for esp on the root softraid device. If not
# found, it's fine in most cases to just create an empty esp and
# let grub handle the magic.
efi_part = utils.get_efi_part_on_device(device)
if efi_part:
efi_part = '{}p{}'.format(device, efi_part)
# We know that we kept this space when configuring raid,see
# hardware.GenericHardwareManager.create_configuration.
# We could also directly get the EFI partition size.
partsize_mib = 128
partlabel_prefix = 'uefi-holder-'
for number, holder in enumerate(holders):
# NOTE: see utils.get_partition_table_type_from_specs
# for uefi we know that we have setup a gpt partition table,
# sgdisk can be used to edit table, more user friendly
# for alignment and relative offsets
partlabel = '{}{}'.format(partlabel_prefix, number)
out, _u = utils.execute('sgdisk', '-F', holder)
start_sector = '{}s'.format(out.splitlines()[-1].strip())
out, _u = utils.execute(
'sgdisk', '-n', '0:{}:+{}MiB'.format(start_sector,
partsize_mib),
'-t', '0:ef00', '-c', '0:{}'.format(partlabel), holder)
# Refresh part table
utils.execute("partprobe")
utils.execute("blkid")
target_part, _u = utils.execute(
"blkid", "-l", "-t", "PARTLABEL={}".format(partlabel), holder)
target_part = target_part.splitlines()[-1].split(':', 1)[0]
LOG.debug("Efi partition %s created on disk holder %s",
target_part, holder)
if efi_part:
LOG.debug("Relocating efi %s to holder part %s", efi_part,
target_part)
# Blockdev copy
utils.execute("cp", efi_part, target_part)
else:
# Creating a label is just to make life easier
if number == 0:
fslabel = 'efi-part'
else:
# bak, label is limited to 11 chars
fslabel = 'efi-part-b'
ilib_utils.mkfs(fs='vfat', path=target_part, label=fslabel)
efi_partitions.append(target_part)
# TBD: Would not hurt to destroy source efi part when defined,
# for clarity.
elif target_boot_mode == 'bios':
partlabel_prefix = 'bios-boot-part-'
for number, holder in enumerate(holders):
label = utils.scan_partition_table_type(holder)
if label == 'gpt':
LOG.debug("Creating bios boot partition on disk holder %s",
holder)
out, _u = utils.execute('sgdisk', '-F', holder)
start_sector = '{}s'.format(out.splitlines()[-1].strip())
partlabel = '{}{}'.format(partlabel_prefix, number)
out, _u = utils.execute(
'sgdisk', '-n', '0:{}:+2MiB'.format(start_sector),
'-t', '0:ef02', '-c', '0:{}'.format(partlabel), holder)
# Q: MBR case, could we dd the boot code from the softraid
# (446 first bytes) if we detect a bootloader with
# _is_bootloader_loaded?
# A: This won't work. Because it includes the address on the
# disk, as in virtual disk, where to load the data from.
# Since there is a structural difference, this means it will
# fail.
# Just an empty list if not uefi boot mode, nvm, not used anyway
return efi_partitions
def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
prep_boot_part_uuid=None):
prep_boot_part_uuid=None, target_boot_mode='bios'):
"""Install GRUB2 bootloader on a given device."""
LOG.debug("Installing GRUB2 bootloader on device %s", device)
efi_partition = None
efi_partitions = None
efi_part = None
efi_partition_mount_point = None
efi_mounted = False
holders = None
# NOTE(TheJulia): Seems we need to get this before ever possibly
# restart the device in the case of multi-device RAID as pyudev
@ -379,12 +494,21 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
LOG.info("Skipping installation of bootloader on device %s "
"as it is already marked bootable.", device)
return
try:
# Mount the partition and binds
path = tempfile.mkdtemp()
if efi_system_part_uuid:
efi_partition = _get_partition(device, uuid=efi_system_part_uuid)
efi_part = _get_partition(device, uuid=efi_system_part_uuid)
efi_partitions = [efi_part]
if hardware.is_md_device(device):
holders = hardware.get_holder_disks(device)
efi_partitions = _prepare_boot_partitions_for_softraid(
device, holders, efi_part, target_boot_mode
)
if efi_partitions:
efi_partition_mount_point = os.path.join(path, "boot/efi")
# For power we want to install grub directly onto the PreP partition
@ -394,7 +518,7 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
# If the root device is an md device (or partition),
# identify the underlying holder disks to install grub.
if hardware.is_md_device(device):
disks = hardware.get_holder_disks(device)
disks = holders
else:
disks = [device]
@ -404,12 +528,6 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
utils.execute('mount', '-t', 'sysfs', 'none', path + '/sys')
if efi_partition:
if not os.path.exists(efi_partition_mount_point):
os.makedirs(efi_partition_mount_point)
utils.execute('mount', efi_partition, efi_partition_mount_point)
efi_mounted = True
binary_name = "grub"
if os.path.exists(os.path.join(path, 'usr/sbin/grub2-install')):
binary_name = "grub2"
@ -419,34 +537,77 @@ def _install_grub2(device, root_uuid, efi_system_part_uuid=None,
# Add /usr/sbin to PATH variable to ensure it is there as we do
# not use full path to grub binary anymore.
path_variable = os.environ.get('PATH', '')
path_variable = '%s:/bin:/usr/sbin' % path_variable
path_variable = '%s:/bin:/usr/sbin:/sbin' % path_variable
# Install grub. Normally, grub goes to one disk only. In case of
# md devices, grub goes to all underlying holder (RAID-1) disks.
LOG.info("GRUB2 will be installed on disks %s", disks)
for grub_disk in disks:
LOG.debug("Installing GRUB2 on disk %s", grub_disk)
utils.execute('chroot %(path)s /bin/sh -c '
'"%(bin)s-install %(dev)s"' %
{'path': path, 'bin': binary_name,
'dev': grub_disk},
shell=True, env_variables={'PATH': path_variable})
LOG.debug("GRUB2 successfully installed on device %s", grub_disk)
if efi_partitions:
if not os.path.exists(efi_partition_mount_point):
os.makedirs(efi_partition_mount_point)
LOG.info("GRUB2 will be installed for UEFI on efi partitions %s",
efi_partitions)
for efi_partition in efi_partitions:
utils.execute(
'mount', efi_partition, efi_partition_mount_point)
efi_mounted = True
# FIXME(rg): does not work in cross boot mode case (target
# boot mode differs from ramdisk one)
# Probe for the correct target (depends on the arch, example
# --target=x86_64-efi)
utils.execute('chroot %(path)s /bin/sh -c '
'"%(bin)s-install"' %
{'path': path, 'bin': binary_name},
shell=True,
env_variables={
'PATH': path_variable
})
# Also run grub-install with --removable, this installs grub to
# the EFI fallback path. Useful if the NVRAM wasn't written
# correctly, was reset or if testing with virt as libvirt
# resets the NVRAM on instance start.
# This operation is essentially a copy operation. Use of the
# --removable flag, per the grub-install source code changes
# the default file to be copied, destination file name, and
# prevents NVRAM from being updated.
# We only run grub2_install for uefi if we can't verify the
# uefi bits
utils.execute('chroot %(path)s /bin/sh -c '
'"%(bin)s-install --removable"' %
{'path': path, 'bin': binary_name},
shell=True,
env_variables={
'PATH': path_variable
})
utils.execute('umount', efi_partition_mount_point, attempts=3,
delay_on_retry=True)
efi_mounted = False
# NOTE: probably never needed for grub-mkconfig, does not hurt in
# case of doubt, cleaned in the finally clause anyway
utils.execute('mount', efi_partitions[0],
efi_partition_mount_point)
efi_mounted = True
else:
# FIXME(rg): does not work if ramdisk boot mode is not the same
# as the target (--target=i386-pc, arch dependent).
# See previous FIXME
# Also run grub-install with --removable, this installs grub to the
# EFI fallback path. Useful if the NVRAM wasn't written correctly,
# was reset or if testing with virt as libvirt resets the NVRAM
# on instance start.
# This operation is essentially a copy operation. Use of the
# --removable flag, per the grub-install source code changes
# the default file to be copied, destination file name, and
# prevents NVRAM from being updated.
# We only run grub2_install for uefi if we can't verify the uefi bits
if efi_partition:
utils.execute('chroot %(path)s /bin/sh -c '
'"%(bin)s-install %(dev)s --removable"' %
{'path': path, 'bin': binary_name, 'dev': device},
shell=True, env_variables={'PATH': path_variable})
# Install grub. Normally, grub goes to one disk only. In case of
# md devices, grub goes to all underlying holder (RAID-1) disks.
LOG.info("GRUB2 will be installed on disks %s", disks)
for grub_disk in disks:
LOG.debug("Installing GRUB2 on disk %s", grub_disk)
utils.execute(
'chroot %(path)s /bin/sh -c "%(bin)s-install %(dev)s"' %
{
'path': path,
'bin': binary_name,
'dev': grub_disk
},
shell=True,
env_variables={
'PATH': path_variable
}
)
LOG.debug("GRUB2 successfully installed on device %s",
grub_disk)
# If the image has dracut installed, set the rd.md.uuid kernel
# parameter for discovered md devices.
@ -527,7 +688,8 @@ class ImageExtension(base.BaseAgentExtension):
@base.sync_command('install_bootloader')
def install_bootloader(self, root_uuid, efi_system_part_uuid=None,
prep_boot_part_uuid=None):
prep_boot_part_uuid=None,
target_boot_mode='bios'):
"""Install the GRUB2 bootloader on the image.
:param root_uuid: The UUID of the root partition.
@ -537,6 +699,9 @@ class ImageExtension(base.BaseAgentExtension):
:param prep_boot_part_uuid: The UUID of the PReP Boot partition.
Used only for booting ppc64* partition images locally. In this
scenario the bootloader will be installed here.
:param target_boot_mode: bios, uefi. Only taken into account
for softraid, when no efi partition is explicitely provided
(happens for whole disk images)
:raises: CommandExecutionError if the installation of the
bootloader fails.
:raises: DeviceNotFound if the root partition is not found.
@ -545,7 +710,8 @@ class ImageExtension(base.BaseAgentExtension):
device = hardware.dispatch_to_managers('get_os_install_device')
iscsi.clean_up(device)
boot = hardware.dispatch_to_managers('get_boot_info')
if boot.current_boot_mode == 'uefi':
if (boot.current_boot_mode == 'uefi'
and not hardware.is_md_device(device)):
has_efibootmgr = True
try:
utils.execute('efibootmgr', '--version')
@ -563,4 +729,5 @@ class ImageExtension(base.BaseAgentExtension):
_install_grub2(device,
root_uuid=root_uuid,
efi_system_part_uuid=efi_system_part_uuid,
prep_boot_part_uuid=prep_boot_part_uuid)
prep_boot_part_uuid=prep_boot_part_uuid,
target_boot_mode=target_boot_mode)

View File

@ -1502,6 +1502,12 @@ class GenericHardwareManager(HardwareManager):
devices.
"""
# incr starts to 1
# It means md0 is on the partition 1, md1 on 2...
# incr could be incremented if we ever decide, for example to create
# some additional partitions here (boot partitions)
incr = 1
raid_config = node.get('target_raid_config', {})
if not raid_config:
LOG.debug("No target_raid_config found")
@ -1544,23 +1550,50 @@ class GenericHardwareManager(HardwareManager):
% ', '.join(with_parts))
raise errors.SoftwareRAIDError(msg)
partition_table_type = utils.get_partition_table_type_from_specs(node)
target_boot_mode = utils.get_node_boot_mode(node)
parted_start_dict = {}
# Create an MBR partition table on each disk.
# TODO(arne_wiebalck): Check if GPT would work as well.
# Create a partition table on each disk.
for dev_name in block_devices:
LOG.info("Creating partition table on {}".format(dev_name))
LOG.info("Creating partition table on {}".format(
dev_name))
try:
utils.execute('parted', dev_name, '-s', '--',
'mklabel', 'msdos')
'mklabel', partition_table_type)
except processutils.ProcessExecutionError as e:
msg = "Failed to create partition table on {}: {}".format(
dev_name, e)
raise errors.SoftwareRAIDError(msg)
out, _u = utils.execute('sgdisk', '-F', dev_name)
# May differ from 2048s, according to device geometry (example:
# 4k disks).
parted_start_dict[dev_name] = "%ss" % out.splitlines()[-1]
# TODO(rg): TBD, several options regarding boot part slots here:
# 1. Create boot partitions in prevision
# 2. Just leave space
# 3. Do nothing: rely on the caller to specify target_raid_config
# correctly according to what they intend to do (eg not set MAX if
# they know they will need some space for bios boot or efi parts.
# (Best option imo, if we accept that the target volume granularity
# is GiB, so you lose up to 1GiB just for a bios boot partition...)
if target_boot_mode == 'uefi':
# Leave 129MiB - start_sector s for the esp (approx 128MiB)
# NOTE: any image efi partition is expected to be less
# than 128MiB
# TBD: 129MiB is a waste in most cases.
raid_start = '129MiB'
else:
if partition_table_type == 'gpt':
# Leave 8MiB - start_sector s (approx 7MiB)
# for the bios boot partition or the ppc prepboot part
# This should avoid grub errors saying that it cannot
# install boot stage 1.5/2 (since the mbr gap does not
# exist on disk holders with gpt tables)
raid_start = '8MiB'
else:
# sgdisk works fine for display data on mbr tables too
out, _u = utils.execute('sgdisk', '-F', dev_name)
raid_start = "{}s".format(out.splitlines()[-1])
parted_start_dict[dev_name] = raid_start
LOG.debug("First available sectors per devices %s", parted_start_dict)
@ -1648,7 +1681,7 @@ class GenericHardwareManager(HardwareManager):
if 'nvme' in device:
part_delimiter = 'p'
component_devices.append(
device + part_delimiter + str(index + 1))
device + part_delimiter + str(index + incr))
raid_level = logical_disk['raid_level']
# The schema check allows '1+0', but mdadm knows it as '10'.
if raid_level == '1+0':

View File

@ -17,6 +17,7 @@ import os
import shutil
import tempfile
from ironic_lib import utils as ilib_utils
import mock
from oslo_concurrency import processutils
@ -59,7 +60,9 @@ class TestImageExtension(base.IronicAgentTest):
self.assertEqual(2, mock_dispatch.call_count)
mock_grub2.assert_called_once_with(
self.fake_dev, root_uuid=self.fake_root_uuid,
efi_system_part_uuid=None, prep_boot_part_uuid=None)
efi_system_part_uuid=None, prep_boot_part_uuid=None,
target_boot_mode='bios'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@ -74,7 +77,9 @@ class TestImageExtension(base.IronicAgentTest):
mock_uefi.return_value = False
self.agent_extension.install_bootloader(
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid)
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi'
)
mock_dispatch.assert_any_call('get_os_install_device')
mock_dispatch.assert_any_call('get_boot_info')
self.assertEqual(2, mock_dispatch.call_count)
@ -82,9 +87,12 @@ class TestImageExtension(base.IronicAgentTest):
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
prep_boot_part_uuid=None)
prep_boot_part_uuid=None,
target_boot_mode='uefi'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(hardware, 'is_md_device', lambda *_: False)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
@ -133,6 +141,7 @@ class TestImageExtension(base.IronicAgentTest):
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
self.assertEqual(8, mock_execute.call_count)
@mock.patch.object(hardware, 'is_md_device', lambda *_: False)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
@ -180,6 +189,7 @@ class TestImageExtension(base.IronicAgentTest):
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
self.assertEqual(8, mock_execute.call_count)
@mock.patch.object(hardware, 'is_md_device', lambda *_: False)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
@ -234,6 +244,7 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
mock_utils_efi_part.assert_called_once_with(self.fake_dev)
self.assertEqual(10, mock_execute.call_count)
@mock.patch.object(hardware, 'is_md_device', lambda *_: False)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
@mock.patch.object(image, '_get_efi_bootloaders', autospec=True)
@ -306,9 +317,12 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
self.fake_dev,
root_uuid=self.fake_root_uuid,
efi_system_part_uuid=None,
prep_boot_part_uuid=self.fake_prep_boot_part_uuid)
prep_boot_part_uuid=self.fake_prep_boot_part_uuid,
target_boot_mode='bios'
)
mock_iscsi_clean.assert_called_once_with(self.fake_dev)
@mock.patch.object(hardware, 'is_md_device', lambda *_: False)
@mock.patch.object(os.path, 'exists', lambda *_: False)
@mock.patch.object(iscsi, 'clean_up', autospec=True)
def test_install_bootloader_failure(self, mock_iscsi_clean, mock_execute,
@ -350,12 +364,14 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s"' %
(self.fake_dir, self.fake_dev)), shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call('umount', self.fake_dir + '/dev',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/proc',
@ -400,12 +416,14 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
'"grub-install %s"' %
(self.fake_dir, self.fake_prep_boot_part)),
shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call('umount', self.fake_dir + '/dev',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/proc',
@ -442,7 +460,8 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
image._install_grub2(
self.fake_dev, root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid)
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi')
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
mock.call('mount', '-o', 'bind', '/dev',
@ -455,19 +474,26 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
self.fake_dir + '/sys'),
mock.call('mount', self.fake_efi_system_part,
self.fake_dir + '/boot/efi'),
mock.call(('chroot %s /bin/sh -c "grub-install"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s"' %
(self.fake_dir, self.fake_dev)), shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s --removable"' %
(self.fake_dir, self.fake_dev)), shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
'"grub-install --removable"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(
'umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('mount', self.fake_efi_system_part,
'/tmp/fake-dir/boot/efi'),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/dev',
@ -525,21 +551,22 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
self.fake_dir + '/sys'),
mock.call('mount', self.fake_efi_system_part,
self.fake_dir + '/boot/efi'),
mock.call(('chroot %s /bin/sh -c "grub-install"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s"' %
(self.fake_dir, self.fake_dev)), shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s --removable"' %
(self.fake_dir, self.fake_dev)), shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
'"grub-install --removable"' % self.fake_dir),
shell=True,
env_variables={'PATH': '/sbin:/bin:/usr/sbin'}),
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
# Call from for loop
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True)]
attempts=3, delay_on_retry=True),
# Call from finally
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True)
]
mock_execute.assert_has_calls(expected)
@mock.patch.object(image, '_is_bootloader_loaded', lambda *_: False)
@ -595,6 +622,374 @@ efibootmgr: ** Warning ** : Boot0005 has same label ironic1\n
uuid=self.fake_root_uuid)
self.assertFalse(mock_dispatch.called)
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
def test__prepare_boot_partitions_for_softraid_uefi_gpt(
self, mock_efi_part, mock_execute, mock_dispatch):
mock_efi_part.return_value = '12'
mock_execute.side_effect = [
('451', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sda12: dsfkgsdjfg', None), # blkid
(None, None), # cp
('452', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sdb14: whatever', None), # blkid
(None, None), # cp
]
efi_parts = image._prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], None,
target_boot_mode='uefi')
mock_efi_part.assert_called_once_with('/dev/md0')
expected = [
mock.call('sgdisk', '-F', '/dev/sda'),
mock.call('sgdisk', '-n', '0:451s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-0', '/dev/sda'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-0',
'/dev/sda'),
mock.call('cp', '/dev/md0p12', '/dev/sda12'),
mock.call('sgdisk', '-F', '/dev/sdb'),
mock.call('sgdisk', '-n', '0:452s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-1', '/dev/sdb'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-1',
'/dev/sdb'),
mock.call('cp', '/dev/md0p12', '/dev/sdb14')
]
mock_execute.assert_has_calls(expected, any_order=False)
self.assertEqual(efi_parts, ['/dev/sda12', '/dev/sdb14'])
@mock.patch.object(utils, 'get_efi_part_on_device', autospec=True)
@mock.patch.object(ilib_utils, 'mkfs', autospec=True)
def test__prepare_boot_partitions_for_softraid_uefi_gpt_esp_not_found(
self, mock_mkfs, mock_efi_part, mock_execute, mock_dispatch):
mock_efi_part.return_value = None
mock_execute.side_effect = [
('451', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sda12: dsfkgsdjfg', None), # blkid
('452', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sdb14: whatever', None), # blkid
]
efi_parts = image._prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], None,
target_boot_mode='uefi')
mock_efi_part.assert_called_once_with('/dev/md0')
expected = [
mock.call('sgdisk', '-F', '/dev/sda'),
mock.call('sgdisk', '-n', '0:451s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-0', '/dev/sda'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-0',
'/dev/sda'),
mock.call('sgdisk', '-F', '/dev/sdb'),
mock.call('sgdisk', '-n', '0:452s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-1', '/dev/sdb'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-1',
'/dev/sdb'),
]
mock_execute.assert_has_calls(expected, any_order=False)
mock_mkfs.assert_has_calls([
mock.call(path='/dev/sda12', label='efi-part', fs='vfat'),
mock.call(path='/dev/sdb14', label='efi-part-b', fs='vfat'),
], any_order=False)
self.assertEqual(efi_parts, ['/dev/sda12', '/dev/sdb14'])
def test__prepare_boot_partitions_for_softraid_uefi_gpt_efi_provided(
self, mock_execute, mock_dispatch):
mock_execute.side_effect = [
('451', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sda12: dsfkgsdjfg', None), # blkid
(None, None), # cp
('452', None), # sgdisk -F
(None, None), # sgdisk create part
(None, None), # partprobe
(None, None), # blkid
('/dev/sdb14: whatever', None), # blkid
(None, None), # cp
]
efi_parts = image._prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], '/dev/md0p15',
target_boot_mode='uefi')
expected = [
mock.call('sgdisk', '-F', '/dev/sda'),
mock.call('sgdisk', '-n', '0:451s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-0', '/dev/sda'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-0',
'/dev/sda'),
mock.call('cp', '/dev/md0p15', '/dev/sda12'),
mock.call('sgdisk', '-F', '/dev/sdb'),
mock.call('sgdisk', '-n', '0:452s:+128MiB', '-t', '0:ef00', '-c',
'0:uefi-holder-1', '/dev/sdb'),
mock.call('partprobe'),
mock.call('blkid'),
mock.call('blkid', '-l', '-t', 'PARTLABEL=uefi-holder-1',
'/dev/sdb'),
mock.call('cp', '/dev/md0p15', '/dev/sdb14')
]
mock_execute.assert_has_calls(expected, any_order=False)
self.assertEqual(efi_parts, ['/dev/sda12', '/dev/sdb14'])
@mock.patch.object(utils, 'scan_partition_table_type', autospec=True,
return_value='msdos')
def test__prepare_boot_partitions_for_softraid_bios_msdos(
self, mock_label_scan, mock_execute, mock_dispatch):
efi_parts = image._prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], 'notusedanyway',
target_boot_mode='bios')
expected = [
mock.call('/dev/sda'),
mock.call('/dev/sdb'),
]
mock_label_scan.assert_has_calls(expected, any_order=False)
self.assertEqual(efi_parts, [])
@mock.patch.object(utils, 'scan_partition_table_type', autospec=True,
return_value='gpt')
def test__prepare_boot_partitions_for_softraid_bios_gpt(
self, mock_label_scan, mock_execute, mock_dispatch):
mock_execute.side_effect = [
('whatever\n314', None), # sgdisk -F
(None, None), # bios boot grub
('warning message\n914', None), # sgdisk -F
(None, None), # bios boot grub
]
efi_parts = image._prepare_boot_partitions_for_softraid(
'/dev/md0', ['/dev/sda', '/dev/sdb'], 'notusedanyway',
target_boot_mode='bios')
expected_scan = [
mock.call('/dev/sda'),
mock.call('/dev/sdb'),
]
mock_label_scan.assert_has_calls(expected_scan, any_order=False)
expected_exec = [
mock.call('sgdisk', '-F', '/dev/sda'),
mock.call('sgdisk', '-n', '0:314s:+2MiB', '-t', '0:ef02', '-c',
'0:bios-boot-part-0', '/dev/sda'),
mock.call('sgdisk', '-F', '/dev/sdb'),
mock.call('sgdisk', '-n', '0:914s:+2MiB', '-t', '0:ef02', '-c',
'0:bios-boot-part-1', '/dev/sdb'),
]
mock_execute.assert_has_calls(expected_exec, any_order=False)
self.assertEqual(efi_parts, [])
@mock.patch.object(image, '_is_bootloader_loaded', lambda *_: True)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(hardware, 'md_restart', autospec=True)
@mock.patch.object(hardware, 'md_get_raid_devices', autospec=True)
@mock.patch.object(hardware, 'get_holder_disks', autospec=True,
return_value=['/dev/sda', '/dev/sdb'])
@mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True)
@mock.patch.object(image, '_prepare_boot_partitions_for_softraid',
autospec=True,
return_value=['/dev/sda1', '/dev/sdb2'])
@mock.patch.object(image, '_has_dracut',
autospec=True,
return_value=False)
def test__install_grub2_softraid_uefi_gpt(
self, mock_dracut,
mock_prepare, mock_get_part_uuid, mkdir_mock, environ_mock,
mock_holder, mock_md_get_raid_devices, mock_restart,
mock_is_md_device,
mock_execute, mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_efi_system_part]
environ_mock.get.return_value = '/sbin'
mock_is_md_device.return_value = True
mock_md_get_raid_devices.return_value = {}
image._install_grub2(
self.fake_dev, root_uuid=self.fake_root_uuid,
efi_system_part_uuid=self.fake_efi_system_part_uuid,
target_boot_mode='uefi')
expected = [mock.call('mount', '/dev/fake2', self.fake_dir),
mock.call('mount', '-o', 'bind', '/dev',
self.fake_dir + '/dev'),
mock.call('mount', '-o', 'bind', '/proc',
self.fake_dir + '/proc'),
mock.call('mount', '-o', 'bind', '/run',
self.fake_dir + '/run'),
mock.call('mount', '-t', 'sysfs', 'none',
self.fake_dir + '/sys'),
mock.call('mount', '/dev/sda1',
self.fake_dir + '/boot/efi'),
mock.call(('chroot %s /bin/sh -c "grub-install"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install --removable"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(
'umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('mount', '/dev/sdb2',
self.fake_dir + '/boot/efi'),
mock.call(('chroot %s /bin/sh -c "grub-install"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install --removable"' %
self.fake_dir), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(
'umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('mount', '/dev/sda1',
'/tmp/fake-dir/boot/efi'),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call('umount', self.fake_dir + '/boot/efi',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/dev',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/proc',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/run',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/sys',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir, attempts=3,
delay_on_retry=True)]
mock_execute.assert_has_calls(expected)
mock_get_part_uuid.assert_any_call(self.fake_dev,
uuid=self.fake_root_uuid)
mock_get_part_uuid.assert_any_call(self.fake_dev,
uuid=self.fake_efi_system_part_uuid)
self.assertFalse(mock_dispatch.called)
mock_prepare.assert_called_once_with(self.fake_dev,
['/dev/sda', '/dev/sdb'],
self.fake_efi_system_part, 'uefi')
mock_restart.assert_called_once_with(self.fake_dev)
mock_holder.assert_called_once_with(self.fake_dev)
mock_dracut.assert_called_once_with(self.fake_dir)
@mock.patch.object(image, '_is_bootloader_loaded', lambda *_: True)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
@mock.patch.object(hardware, 'md_restart', autospec=True)
@mock.patch.object(hardware, 'md_get_raid_devices', autospec=True)
@mock.patch.object(hardware, 'get_holder_disks', autospec=True,
return_value=['/dev/sda', '/dev/sdb'])
@mock.patch.object(os, 'environ', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(image, '_get_partition', autospec=True)
@mock.patch.object(image, '_prepare_boot_partitions_for_softraid',
autospec=True,
return_value=[])
@mock.patch.object(image, '_has_dracut',
autospec=True,
return_value=False)
def test__install_grub2_softraid_bios(
self, mock_dracut,
mock_prepare, mock_get_part_uuid, mkdir_mock, environ_mock,
mock_holder, mock_md_get_raid_devices, mock_restart,
mock_is_md_device,
mock_execute, mock_dispatch):
mock_get_part_uuid.side_effect = [self.fake_root_part,
self.fake_efi_system_part]
environ_mock.get.return_value = '/sbin'
mock_is_md_device.return_value = True
mock_md_get_raid_devices.return_value = {}
image._install_grub2(
self.fake_dev, root_uuid=self.fake_root_uuid,
efi_system_part_uuid=None,
target_boot_mode='bios')
expected = [
mock.call('mount', '/dev/fake2', self.fake_dir),
mock.call('mount', '-o', 'bind', '/dev',
self.fake_dir + '/dev'),
mock.call('mount', '-o', 'bind', '/proc',
self.fake_dir + '/proc'),
mock.call('mount', '-o', 'bind', '/run',
self.fake_dir + '/run'),
mock.call('mount', '-t', 'sysfs', 'none',
self.fake_dir + '/sys'),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s"' %
(self.fake_dir, '/dev/sda')), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-install %s"' %
(self.fake_dir, '/dev/sdb')), shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call(('chroot %s /bin/sh -c '
'"grub-mkconfig -o '
'/boot/grub/grub.cfg"' % self.fake_dir),
shell=True,
env_variables={
'PATH': '/sbin:/bin:/usr/sbin:/sbin'}),
mock.call('umount', self.fake_dir + '/dev',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/proc',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/run',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir + '/sys',
attempts=3, delay_on_retry=True),
mock.call('umount', self.fake_dir, attempts=3,
delay_on_retry=True)]
self.assertFalse(mkdir_mock.called)
mock_execute.assert_has_calls(expected)
mock_get_part_uuid.assert_any_call(self.fake_dev,
uuid=self.fake_root_uuid)
self.assertFalse(mock_dispatch.called)
mock_prepare.assert_called_once_with(self.fake_dev,
['/dev/sda', '/dev/sdb'],
None, 'bios')
mock_restart.assert_called_once_with(self.fake_dev)
mock_holder.assert_called_once_with(self.fake_dev)
mock_dracut.assert_called_once_with(self.fake_dir)
@mock.patch.object(image, '_is_bootloader_loaded', autospec=True)
@mock.patch.object(hardware, 'is_md_device', autospec=True)
def test__get_partition(self, mock_is_md_device, mock_is_bootloader,

View File

@ -2781,8 +2781,11 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration(self, mocked_execute, mock_list_parts):
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration(self, mocked_os_path_isdir, mocked_execute,
mock_list_parts):
node = self.node
raid_config = {
"logical_disks": [
{
@ -2820,13 +2823,13 @@ class TestGenericHardwareManager(base.IronicAgentTest):
]
result = self.hardware.create_configuration(node, [])
mocked_os_path_isdir.assert_has_calls([
mock.call('/sys/firmware/efi')
])
mocked_execute.assert_has_calls([
mock.call('parted', '/dev/sda', '-s', '--', 'mklabel',
'msdos'),
mock.call('parted', '/dev/sda', '-s', '--', 'mklabel', 'msdos'),
mock.call('sgdisk', '-F', '/dev/sda'),
mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel',
'msdos'),
mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel', 'msdos'),
mock.call('sgdisk', '-F', '/dev/sdb'),
mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '42s', '10GiB'),
@ -3034,7 +3037,144 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_no_max(self, mocked_execute,
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=True)
def test_create_configuration_efi(self, mocked_os_path_isdir,
mocked_execute, mock_list_parts):
node = self.node
raid_config = {
"logical_disks": [
{
"size_gb": "10",
"raid_level": "1",
"controller": "software",
},
{
"size_gb": "MAX",
"raid_level": "0",
"controller": "software",
},
]
}
node['target_raid_config'] = raid_config
device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True)
device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True)
self.hardware.list_block_devices = mock.Mock()
self.hardware.list_block_devices.return_value = [device1, device2]
mocked_execute.side_effect = [
None, # mklabel sda
None, # mklabel sda
None, None, # parted + partx sda
None, None, # parted + partx sdb
None, None, # parted + partx sda
None, None, # parted + partx sdb
None, None # mdadms
]
result = self.hardware.create_configuration(node, [])
mocked_os_path_isdir.assert_has_calls([
mock.call('/sys/firmware/efi')
])
mocked_execute.assert_has_calls([
mock.call('parted', '/dev/sda', '-s', '--', 'mklabel', 'gpt'),
mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel', 'gpt'),
mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '129MiB', '10GiB'),
mock.call('partx', '-u', '/dev/sda', check_exit_code=False),
mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '129MiB', '10GiB'),
mock.call('partx', '-u', '/dev/sdb', check_exit_code=False),
mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '10GiB', '-1'),
mock.call('partx', '-u', '/dev/sda', check_exit_code=False),
mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '10GiB', '-1'),
mock.call('partx', '-u', '/dev/sdb', check_exit_code=False),
mock.call('mdadm', '--create', '/dev/md0', '--force', '--run',
'--metadata=1', '--level', '1', '--raid-devices', 2,
'/dev/sda1', '/dev/sdb1'),
mock.call('mdadm', '--create', '/dev/md1', '--force', '--run',
'--metadata=1', '--level', '0', '--raid-devices', 2,
'/dev/sda2', '/dev/sdb2')])
self.assertEqual(raid_config, result)
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_force_gpt_with_disk_label(
self, mocked_os_path_isdir, mocked_execute, mock_list_part):
node = self.node
raid_config = {
"logical_disks": [
{
"size_gb": "10",
"raid_level": "1",
"controller": "software",
},
{
"size_gb": "MAX",
"raid_level": "0",
"controller": "software",
},
]
}
node['target_raid_config'] = raid_config
node['properties'] = {
'capabilities': {
'disk_label': 'gpt'
}
}
device1 = hardware.BlockDevice('/dev/sda', 'sda', 107374182400, True)
device2 = hardware.BlockDevice('/dev/sdb', 'sdb', 107374182400, True)
self.hardware.list_block_devices = mock.Mock()
self.hardware.list_block_devices.return_value = [device1, device2]
mocked_execute.side_effect = [
None, # mklabel sda
None, # mklabel sda
None, None, # parted + partx sda
None, None, # parted + partx sdb
None, None, # parted + partx sda
None, None, # parted + partx sdb
None, None # mdadms
]
result = self.hardware.create_configuration(node, [])
mocked_os_path_isdir.assert_has_calls([
mock.call('/sys/firmware/efi')
])
mocked_execute.assert_has_calls([
mock.call('parted', '/dev/sda', '-s', '--', 'mklabel', 'gpt'),
mock.call('parted', '/dev/sdb', '-s', '--', 'mklabel', 'gpt'),
mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '8MiB', '10GiB'),
mock.call('partx', '-u', '/dev/sda', check_exit_code=False),
mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '8MiB', '10GiB'),
mock.call('partx', '-u', '/dev/sdb', check_exit_code=False),
mock.call('parted', '/dev/sda', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '10GiB', '-1'),
mock.call('partx', '-u', '/dev/sda', check_exit_code=False),
mock.call('parted', '/dev/sdb', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '10GiB', '-1'),
mock.call('partx', '-u', '/dev/sdb', check_exit_code=False),
mock.call('mdadm', '--create', '/dev/md0', '--force', '--run',
'--metadata=1', '--level', '1', '--raid-devices', 2,
'/dev/sda1', '/dev/sdb1'),
mock.call('mdadm', '--create', '/dev/md1', '--force', '--run',
'--metadata=1', '--level', '0', '--raid-devices', 2,
'/dev/sda2', '/dev/sdb2')])
self.assertEqual(raid_config, result)
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_no_max(self, _mocked_isdir, mocked_execute,
mock_list_parts):
node = self.node
raid_config = {
@ -3100,7 +3240,9 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_max_is_first_logical(self, mocked_execute,
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_max_is_first_logical(self, _mocked_isdir,
mocked_execute,
mock_list_parts):
node = self.node
raid_config = {
@ -3247,7 +3389,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_invalid_raid_config(self, mocked_execute):
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_invalid_raid_config(self,
mocked_os_path_is_dir,
mocked_execute):
raid_config = {
"logical_disks": [
{
@ -3323,8 +3468,12 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True)
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_partitions_detected(self, mocked_execute,
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_partitions_detected(self,
mocked_os_path_is_dir,
mocked_execute,
mock_list_parts):
raid_config = {
"logical_disks": [
{
@ -3358,9 +3507,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_device_handling_failures(self,
mocked_execute,
mock_list_parts):
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=False)
def test_create_configuration_device_handling_failures(
self, mocked_os_path_is_dir, mocked_execute, mock_list_parts):
raid_config = {
"logical_disks": [
{
@ -3501,8 +3651,9 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_with_nvme(self, mocked_execute,
mock_list_parts):
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=True)
def test_create_configuration_with_nvme(self, mocked_os_path_isdir,
mocked_execute, mock_list_parts):
raid_config = {
"logical_disks": [
{
@ -3527,9 +3678,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_execute.side_effect = [
None, # mklabel sda
("WARNING MBR NOT GPT\n42", None), # sgdisk -F sda
None, # mklabel sda
("WARNING MBR NOT GPT\n42", None), # sgdisk -F sdb
None, None, # parted + partx sda
None, None, # parted + partx sdb
None, None, # parted + partx sda
@ -3541,16 +3690,14 @@ class TestGenericHardwareManager(base.IronicAgentTest):
mocked_execute.assert_has_calls([
mock.call('parted', '/dev/nvme0n1', '-s', '--', 'mklabel',
'msdos'),
mock.call('sgdisk', '-F', '/dev/nvme0n1'),
'gpt'),
mock.call('parted', '/dev/nvme1n1', '-s', '--', 'mklabel',
'msdos'),
mock.call('sgdisk', '-F', '/dev/nvme1n1'),
'gpt'),
mock.call('parted', '/dev/nvme0n1', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '42s', '10GiB'),
'mkpart', 'primary', '129MiB', '10GiB'),
mock.call('partx', '-u', '/dev/nvme0n1', check_exit_code=False),
mock.call('parted', '/dev/nvme1n1', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '42s', '10GiB'),
'mkpart', 'primary', '129MiB', '10GiB'),
mock.call('partx', '-u', '/dev/nvme1n1', check_exit_code=False),
mock.call('parted', '/dev/nvme0n1', '-s', '-a', 'optimal', '--',
'mkpart', 'primary', '10GiB', '-1'),
@ -3569,7 +3716,10 @@ class TestGenericHardwareManager(base.IronicAgentTest):
@mock.patch.object(disk_utils, 'list_partitions', autospec=True,
return_value=[])
@mock.patch.object(utils, 'execute', autospec=True)
def test_create_configuration_failure_with_nvme(self, mocked_execute,
@mock.patch.object(os.path, 'isdir', autospec=True, return_value=True)
def test_create_configuration_failure_with_nvme(self,
mocked_os_path_isdir,
mocked_execute,
mock_list_parts):
raid_config = {
"logical_disks": [
@ -3613,9 +3763,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
error_regex = "Failed to create partitions on /dev/nvme0n1"
mocked_execute.side_effect = [
None, # partition tables on sda
('42', None), # sgdisk -F sda
None, # partition tables on sdb
('42', None), # sgdisk -F sdb
processutils.ProcessExecutionError]
self.assertRaisesRegex(errors.SoftwareRAIDError, error_regex,
self.hardware.create_configuration,
@ -3625,9 +3773,7 @@ class TestGenericHardwareManager(base.IronicAgentTest):
"on /dev/nvme0n1p1 /dev/nvme1n1p1")
mocked_execute.side_effect = [
None, # partition tables on sda
('42', None), # sgdisk -F sda
None, # partition tables on sdb
('42', None), # sgdisk -F sdb
None, None, None, None, # RAID-1 partitions on sd{a,b} + partx
None, None, None, None, # RAID-N partitions on sd{a,b} + partx
processutils.ProcessExecutionError]

View File

@ -644,7 +644,7 @@ class TestUtils(testtools.TestCase):
@mock.patch.object(utils, 'execute', autospec=True)
def test_get_efi_part_on_device_without_fs(self, mocked_execute):
parted_ret = PARTED_OUTPUT_UNFORMATTED_NOFS.format('gpt')
parted_ret = PARTED_OUTPUT_UNFORMATTED_NOFS
mocked_execute.side_effect = [
(parted_ret, None)
]
@ -664,6 +664,267 @@ class TestUtils(testtools.TestCase):
[mock.call('parted', '-s', '/dev/sda', '--', 'print')]
)
def test_extract_capability_from_dict(self):
expected_dict = {"hello": "world"}
root = {"capabilities": expected_dict}
self.assertDictEqual(
expected_dict,
utils.parse_capabilities(root))
def test_extract_capability_from_json_string(self):
root = {'capabilities': '{"test": "world"}'}
self.assertDictEqual(
{"test": "world"},
utils.parse_capabilities(root))
def test_extract_capability_from_old_format_caps(self):
root = {'capabilities': 'test:world:2,hello:test1,badformat'}
self.assertDictEqual(
{'hello': 'test1'},
utils.parse_capabilities(root))
@mock.patch.object(os.path, 'isdir', return_value=True, autospec=True)
def test_boot_mode_fallback_uefi(self, mock_os):
node = {}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_fallback_bios(self, mock_os):
node = {}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('bios', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_from_driver_internal_info(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'uefi'
},
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_from_properties_str(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': 'boot_mode:uefi'
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_from_properties_dict(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': {
'boot_mode': 'uefi'
}
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_from_properties_json_str(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': '{"boot_mode": "uefi"}'
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_override_with_instance_info(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': {
'boot_mode': 'bios'
}
},
'instance_info': {
'deploy_boot_mode': 'uefi'
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_boot_mode_implicit_with_secure_boot(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': {
'boot_mode': 'bios',
'secure_boot': 'TrUe'
}
},
'instance_info': {
'deploy_boot_mode': 'bios'
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_has_calls([])
@mock.patch.object(os.path, 'isdir', return_value=False, autospec=True)
def test_secure_boot_overriden_with_instance_info_caps(self, mock_os):
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': {
'boot_mode': 'bios',
'secure_boot': 'false'
}
},
'instance_info': {
'deploy_boot_mode': 'bios',
'capabilities': {
'secure_boot': 'true'
}
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_has_calls([])
@mock.patch.object(os.path, 'isdir', return_value=True, autospec=True)
def test_boot_mode_invalid_cap(self, mock_os):
# In case of invalid boot mode specified we fallback to ramdisk boot
# mode
node = {
'driver_internal_info': {
'deploy_boot_mode': 'bios'
},
'properties': {
'capabilities': {
'boot_mode': 'sfkshfks'
}
}
}
boot_mode = utils.get_node_boot_mode(node)
self.assertEqual('uefi', boot_mode)
mock_os.assert_called_once_with('/sys/firmware/efi')
@mock.patch.object(utils, 'get_node_boot_mode', return_value='bios',
autospec=True)
def test_specified_partition_table_type(self, mock_boot_mode):
node = {}
label = utils.get_partition_table_type_from_specs(node)
self.assertEqual('msdos', label)
mock_boot_mode.assert_called_once_with(node)
@mock.patch.object(utils, 'get_node_boot_mode', return_value='uefi',
autospec=True)
def test_specified_partition_table_type_gpt(self, mock_boot_mode):
node = {}
label = utils.get_partition_table_type_from_specs(node)
self.assertEqual('gpt', label)
mock_boot_mode.assert_called_once_with(node)
@mock.patch.object(utils, 'get_node_boot_mode', return_value='bios',
autospec=True)
def test_specified_partition_table_type_with_disk_label(self,
mock_boot_mode):
node = {
'properties': {
'capabilities': 'disk_label:gpt'
}
}
label = utils.get_partition_table_type_from_specs(node)
self.assertEqual('gpt', label)
mock_boot_mode.assert_has_calls([])
@mock.patch.object(utils, 'get_node_boot_mode', return_value='bios',
autospec=True)
def test_specified_partition_table_type_with_instance_disk_label(
self, mock_boot_mode):
# In case of invalid boot mode specified we fallback to ramdisk boot
# mode
node = {
'instance_info': {
'capabilities': 'disk_label:gpt'
}
}
label = utils.get_partition_table_type_from_specs(node)
self.assertEqual('gpt', label)
mock_boot_mode.assert_has_calls([])
@mock.patch.object(utils, 'get_node_boot_mode', return_value='uefi',
autospec=True)
def test_specified_partition_table_type_disk_label_ignored_with_uefi(
self, mock_boot_mode):
# In case of invalid boot mode specified we fallback to ramdisk boot
# mode
node = {
'instance_info': {
'capabilities': 'disk_label:msdos'
}
}
label = utils.get_partition_table_type_from_specs(node)
self.assertEqual('gpt', label)
mock_boot_mode.assert_has_calls([])
@mock.patch.object(utils, 'execute', autospec=True)
def test_scan_partition_table_type_gpt(self, mocked_execute):
self._test_scan_partition_table_by_type(mocked_execute, 'gpt', 'gpt')
@mock.patch.object(utils, 'execute', autospec=True)
def test_scan_partition_table_type_msdos(self, mocked_execute):
self._test_scan_partition_table_by_type(mocked_execute, 'msdos',
'msdos')
@mock.patch.object(utils, 'execute', autospec=True)
def test_scan_partition_table_type_unknown(self, mocked_execute):
self._test_scan_partition_table_by_type(mocked_execute, 'whatever',
'unknown')
def _test_scan_partition_table_by_type(self, mocked_execute,
table_type_output,
expected_table_type):
parted_ret = PARTED_OUTPUT_UNFORMATTED.format(table_type_output)
mocked_execute.side_effect = [
(parted_ret, None),
]
ret = utils.scan_partition_table_type('hello')
mocked_execute.assert_has_calls(
[mock.call('parted', '-s', 'hello', '--', 'print')]
)
self.assertEqual(expected_table_type, ret)
class TestRemoveKeys(testtools.TestCase):
def test_remove_keys(self):

View File

@ -30,6 +30,7 @@ from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import base64
from oslo_serialization import jsonutils
from oslo_utils import units
from ironic_python_agent import errors
@ -70,6 +71,8 @@ COLLECT_LOGS_COMMANDS = {
DEVICE_EXTRACTOR = re.compile(r'^(?:(.*\d)p|(.*\D))(?:\d+)$')
PARTED_TABLE_TYPE_REGEX = re.compile(r'^.*partition\s+table\s*:\s*(gpt|msdos)',
re.IGNORECASE)
PARTED_ESP_PATTERN = re.compile(r'^\s*(\d+)\s.*\s\s.*\s.*esp(,|\s|$).*$')
@ -454,6 +457,159 @@ def extract_device(part):
return (m.group(1) or m.group(2))
# See ironic.drivers.utils.get_node_capability
def _parse_capabilities_str(cap_str):
"""Extract capabilities from string.
:param cap_str: string meant to meet key1:value1,key2:value2 format
:return: a dictionnary
"""
LOG.debug("Parsing capability string %s", cap_str)
capabilities = {}
for node_capability in cap_str.split(','):
parts = node_capability.split(':')
if len(parts) == 2 and parts[0] and parts[1]:
capabilities[parts[0]] = parts[1]
else:
LOG.warning("Ignoring malformed capability '%s'. "
"Format should be 'key:val'.", node_capability)
LOG.debug("Parsed capabilities %s", capabilities)
return capabilities
# See ironic.common.utils.parse_instance_info_capabilities. Same except that
# we do not handle node.properties.capabilities and
# node.instance_info.capabilities differently
def parse_capabilities(root):
"""Extract capabilities from provided root dictionary-behaving object.
root.get('capabilities', {}) value can either be a dict, or a json str, or
a key1:value1,key2:value2 formatted string.
:param root: Anything behaving like a dict and containing capabilities
formatted as expected. Can be node.get('properties', {}),
node.get('instance_info', {}).
:returns: A dictionary with the capabilities if found and well formatted,
otherwise an empty dictionary.
"""
capabilities = root.get('capabilities', {})
if isinstance(capabilities, str):
try:
capabilities = jsonutils.loads(capabilities)
except (ValueError, TypeError):
capabilities = _parse_capabilities_str(capabilities)
if not isinstance(capabilities, dict):
LOG.warning("Invalid capabilities %s", capabilities)
return {}
return capabilities
def _is_secure_boot(instance_info_caps, node_caps):
"""Extract node secure boot property"""
return 'true' == str(instance_info_caps.get(
'secure_boot', node_caps.get('secure_boot', 'false'))).lower()
# TODO(rg): This method should be mutualized with the one found in
# ironic.drivers.modules.boot_mode_utils.
# The only difference here:
# 1. node is a dict, not an ironic.objects.node
# 2. implicit bios boot mode when using trusted boot capability is removed:
# there is no reason why trusted_boot should imply bios boot mode.
def get_node_boot_mode(node):
"""Returns the node boot mode.
It returns 'uefi' if 'secure_boot' is set to 'true' in
'instance_info/capabilities' of node. Otherwise it directly look for boot
mode hints into
:param node: dictionnary.
:returns: 'bios' or 'uefi'
"""
instance_info = node.get('instance_info', {})
instance_info_caps = parse_capabilities(instance_info)
node_caps = parse_capabilities(node.get('properties', {}))
if _is_secure_boot(instance_info_caps, node_caps):
LOG.debug('Deploy boot mode is implicitely uefi for because secure '
'boot is activated.')
return 'uefi'
ramdisk_boot_mode = 'uefi' if os.path.isdir('/sys/firmware/efi') \
else 'bios'
# Priority order implemented in ironic
boot_mode = instance_info.get(
'deploy_boot_mode',
node_caps.get(
'boot_mode',
node.get('driver_internal_info', {}).get('deploy_boot_mode',
ramdisk_boot_mode))
)
boot_mode = str(boot_mode).lower()
if boot_mode not in ['uefi', 'bios']:
boot_mode = ramdisk_boot_mode
LOG.debug('Deploy boot mode: %s', boot_mode)
return boot_mode
def get_partition_table_type_from_specs(node):
"""Returns the node partition label, gpt or msdos.
If boot mode is uefi, return gpt. Else, choice is open, look for
disk_label capabilities (instance_info has priority over properties).
:param node:
:return: gpt or msdos
"""
instance_info_caps = parse_capabilities(node.get('instance_info', {}))
node_caps = parse_capabilities(node.get('properties', {}))
# Let's not make things more complicated than they already are.
# We currently just ignore the specified disk label in case of uefi,
# and force gpt, even if msdos is possible. Small amends needed if ever
# needed (doubt that)
boot_mode = get_node_boot_mode(node)
if boot_mode == 'uefi':
return 'gpt'
disk_label = instance_info_caps.get(
'disk_label',
node_caps.get('disk_label', 'msdos')
)
return 'gpt' if disk_label == 'gpt' else 'msdos'
def scan_partition_table_type(device):
"""Get partition table type, msdos or gpt.
:param device_name: the name of the device
:return: msdos, gpt or unknown
"""
out, _u = execute('parted', '-s', device, '--', 'print')
out = out.splitlines()
for line in out:
m = PARTED_TABLE_TYPE_REGEX.match(line)
if m:
return m.group(1)
LOG.warning("Unable to get partition table type for device %s.",
device)
return 'unknown'
def get_efi_part_on_device(device):
"""Looks for the efi partition on a given device

View File

@ -0,0 +1,12 @@
---
features:
|
- Adds UEFI boot support for Software RAID, and for partition table
creation based upon boot mode in use.
upgrade:
|
- The type of the partition table created for Software RAID is now based
upon the boot mode in use (GPT for UEFI or if explicitly passed via the
instance's capabilities or the node's properties, otherwise MSDOS).
- The amount of reserved space on the drives now also depends on the boot
mode (128MiB for UEFI/GPT, 8MiB for BIOS/GPT, and one sector otherwise).