Skip to content

Commit

Permalink
Add support for ISO boot via systemd-boot
Browse files Browse the repository at this point in the history
Create proper EFI FAT image via bootctl to be used as
alt loader in xorriso. This allows to boot the ISO
via EFI e.g kvm -bios /usr/share/qemu/ovmf-x86_64.bin -cdrom file.iso
Please note, hybrid boot is done via grub's hybrid MBR and
as systemd-boot does not provide one, hybrid boot is out
of scope yet. This Fixes #2281
  • Loading branch information
schaefi committed Nov 7, 2023
1 parent ea466ed commit d6d57ea
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 73 deletions.
25 changes: 19 additions & 6 deletions build-tests/x86/tumbleweed/test-image-live/appliance.kiwi
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<profile name="BIOS" description="BIOS only Live Boot based on isolinux"/>
<profile name="Standard" description="Standard EFI/BIOS Live Boot"/>
<profile name="Secure" description="SecureBoot/BIOS Live Boot"/>
<profile name="SDBoot" description="EFI Boot via systemd-boot"/>
</profiles>
<preferences>
<version>1.42.3</version>
Expand All @@ -31,6 +32,11 @@
<bootloader name="isolinux" console="serial" timeout="10"/>
</type>
</preferences>
<preferences profiles="SDBoot">
<type image="iso" flags="overlay" firmware="efi" kernelcmdline="console=ttyS0" hybridpersistent_filesystem="ext4" hybridpersistent="true" mediacheck="false">
<bootloader name="systemd_boot"/>
</type>
</preferences>
<preferences profiles="Standard">
<type image="iso" flags="overlay" firmware="efi" kernelcmdline="console=ttyS0" hybridpersistent_filesystem="ext4" hybridpersistent="true" mediacheck="true">
<bootloader name="grub2" console="serial" timeout="10"/>
Expand All @@ -47,19 +53,27 @@
<repository type="rpm-md">
<source path="obsrepositories:/"/>
</repository>
<packages type="image" profiles="SDBoot">
<package name="systemd-boot"/>
</packages>
<packages type="image" profiles="Standard,Secure">
<package name="grub2-branding-openSUSE"/>
<package name="grub2"/>
<package name="grub2-x86_64-efi" arch="x86_64"/>
<package name="grub2-i386-pc"/>
<package name="shim"/>
</packages>
<packages type="image" profiles="BIOS">
<package name="syslinux"/>
</packages>
<packages type="image">
<package name="bind-utils"/>
<package name="patterns-openSUSE-base"/>
<package name="systemd"/>
<package name="plymouth-theme-breeze"/>
<package name="plymouth-plugin-script"/>
<package name="grub2-branding-openSUSE"/>
<package name="iputils"/>
<package name="vim"/>
<package name="grub2"/>
<package name="grub2-x86_64-efi" arch="x86_64"/>
<package name="grub2-i386-pc"/>
<package name="syslinux"/>
<package name="lvm2"/>
<package name="plymouth"/>
<package name="fontconfig"/>
Expand All @@ -73,7 +87,6 @@
<package name="dhcp-client"/>
<package name="which"/>
<package name="kernel-default"/>
<package name="shim"/>
<package name="timezone"/>
<package name="dracut-kiwi-live"/>
</packages>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<rpm-check-signatures>false</rpm-check-signatures>
<bootsplash-theme>breeze</bootsplash-theme>
<bootloader-theme>openSUSE</bootloader-theme>
<type image="oem" filesystem="xfs" kernelcmdline="console=ttyS0" firmware="efi" efipartsize="200">
<type image="oem" filesystem="xfs" kernelcmdline="console=ttyS0" firmware="efi" efipartsize="512" installiso="true">
<bootloader name="systemd_boot" timeout="10"/>
<oemconfig>
<oem-resize>false</oem-resize>
Expand All @@ -30,6 +30,7 @@
<source path="obsrepositories:/"/>
</repository>
<packages type="image">
<package name="dracut-kiwi-oem-dump"/>
<package name="patterns-openSUSE-base"/>
<package name="systemd"/>
<package name="systemd-boot"/>
Expand Down
1 change: 0 additions & 1 deletion kiwi/bootloader/config/bootloader_spec_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,4 @@ def _setup_iso_image_config(
self.custom_args['kernel'] = kernel
self.custom_args['initrd'] = initrd

self.setup_loader(target)
self.set_loader_entry(target)
124 changes: 99 additions & 25 deletions kiwi/bootloader/config/systemd_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
from kiwi.bootloader.template.systemd_boot import BootLoaderTemplateSystemdBoot
from kiwi.bootloader.config.bootloader_spec_base import BootLoaderSpecBase
from kiwi.boot.image.base import BootImageBase
from kiwi.mount_manager import MountManager
from kiwi.storage.loop_device import LoopDevice
from kiwi.storage.disk import Disk
from kiwi.command import Command

from kiwi.exceptions import (
Expand Down Expand Up @@ -73,49 +76,120 @@ def setup_loader(self, target: str) -> None:
boot_options.get('efi_device'),
boot_options.get('system_volumes')
)
self._run_bootctl(self.root_mount.mountpoint)

def set_loader_entry(self, target: str) -> None:
"""
Setup/update loader entries
loader entries for systemd-boot are expected to be managed
through kernel-install. Thus no further action needs to be
taken by kiwi
:param str target:
target identifier, one of disk, live(iso) or install(iso)
"""
pass

def _create_embedded_fat_efi_image(self, path: str):
"""
Creates a EFI system partition image at the given path.
Installs systemd-boot required EFI layout into the image
"""
# NOTE: limitation of El Torito boot,
# if the ESP size is greater than 20MB, the machine
# does no longer boot. I could not find a solution to
# this issue and bootctl install + kernel-install needs
# ~200MB for the ESP. I also noted that kernel-install
# does not fail with an exit code != if there is no
# space left anymore. Thus the image build completes but
# with incomplete data on the ESP
fat_image_mbsize = int(
self.xml_state.build_type.get_efifatimagesize() or 20
)
Command.run(
['qemu-img', 'create', path, f'{fat_image_mbsize}M']
)
Command.run(
['sgdisk', '-n', ':1.0', '-t', '1:EF00', path]
)
loop_provider = LoopDevice(path)
loop_provider.create(overwrite=False)
disk = Disk('gpt', loop_provider)
disk.map_partitions()
disk.partitioner.partition_id = 1
disk._add_to_map('efi')
Command.run(
['mkdosfs', '-n', 'BOOT', disk.partition_map['efi']]
)
Path.create(f'{self.root_dir}/boot/efi')
efi_mount = MountManager(
device=disk.partition_map['efi'],
mountpoint=f'{self.root_dir}/boot/efi'
)
device_mount = MountManager(
device='/dev',
mountpoint=f'{self.root_dir}/dev'
)
proc_mount = MountManager(
device='/proc',
mountpoint=f'{self.root_dir}/proc'
)
sys_mount = MountManager(
device='/sys',
mountpoint=f'{self.root_dir}/sys'
)
efi_mount.mount()
device_mount.bind_mount()
proc_mount.bind_mount()
sys_mount.bind_mount()
try:
self._run_bootctl(self.root_dir)
finally:
efi_mount.umount()
device_mount.umount()
proc_mount.umount()
sys_mount.umount()
Command.run(
['dd', f'if={disk.partition_map["efi"]}', f'of={path}.img']
)
del disk
del loop_provider
Command.run(
['mv', f'{path}.img', path]
)

def _run_bootctl(self, root_dir: str) -> None:
"""
Setup ESP for systemd-boot using bootctl and kernel-install
"""
kernel_info = BootImageBase(
self.xml_state,
self.root_mount.mountpoint, self.root_mount.mountpoint
self.xml_state, root_dir, root_dir
).get_boot_names()
Command.run(
[
'chroot', self.root_mount.mountpoint, 'bootctl', 'install',
'chroot', root_dir, 'bootctl', 'install',
'--esp-path=/boot/efi',
'--no-variables',
'--entry-token', 'os-id'
]
)
Path.wipe(f'{self.root_mount.mountpoint}/boot/loader')
self._write_kernel_cmdline_file()
Path.wipe(f'{root_dir}/boot/loader')
self._write_kernel_cmdline_file(root_dir)
Command.run(
[
'chroot', self.root_mount.mountpoint,
'chroot', root_dir,
'kernel-install', 'add', kernel_info.kernel_version,
kernel_info.kernel_filename.replace(
self.root_mount.mountpoint, ''
),
kernel_info.kernel_filename.replace(root_dir, ''),
f'/boot/{kernel_info.initrd_name}'
]
)
BootLoaderSystemdBoot._write_config_file(
BootLoaderTemplateSystemdBoot().get_loader_template(),
self.root_mount.mountpoint + '/boot/efi/loader/loader.conf',
root_dir + '/boot/efi/loader/loader.conf',
self._get_template_parameters()
)

def set_loader_entry(self, target: str) -> None:
"""
Setup/update loader entries
loader entries for systemd-boot are expected to be managed
through kernel-install. Thus no further action needs to be
taken by kiwi
:param str target:
target identifier, one of disk, live(iso) or install(iso)
"""
pass

def _get_template_parameters(self) -> Dict[str, str]:
return {
'kernel_file': self.custom_args['kernel'] or 'vmlinuz',
Expand All @@ -140,8 +214,8 @@ def _write_config_file(
'{0}: {1}'.format(type(issue).__name__, issue)
)

def _write_kernel_cmdline_file(self) -> None:
kernel_cmdline = f'{self.root_mount.mountpoint}/etc/kernel/cmdline'
def _write_kernel_cmdline_file(self, root_dir: str) -> None:
kernel_cmdline = f'{root_dir}/etc/kernel/cmdline'
Path.create(os.path.dirname(kernel_cmdline))
with open(kernel_cmdline, 'w') as cmdline:
cmdline.write(self.cmdline)
19 changes: 11 additions & 8 deletions kiwi/builder/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def __init__(
custom_args: Dict = None
) -> None:
self.arch = Defaults.get_platform_name()
self.bootloader = xml_state.get_build_type_bootloader_name()
if self.bootloader != 'systemd_boot':
self.bootloader = 'grub2'
self.root_dir = root_dir
self.target_dir = target_dir
self.xml_state = xml_state
Expand Down Expand Up @@ -210,7 +213,7 @@ def create_install_iso(self) -> None:
# for compat boot. The complete bootloader setup will be
# based on grub
bootloader_config = BootLoaderConfig.new(
'grub2', self.xml_state, root_dir=self.root_dir,
self.bootloader, self.xml_state, root_dir=self.root_dir,
boot_dir=self.media_dir.name, custom_args={
'grub_directory_name':
Defaults.get_grub_boot_directory_name(self.root_dir),
Expand Down Expand Up @@ -242,20 +245,20 @@ def create_install_iso(self) -> None:
)
bootloader_config.write()

if self.firmware.efi_mode():
efi_loader = Temporary(
prefix='efi-loader.', path=self.target_dir
).new_file()
bootloader_config._create_embedded_fat_efi_image(efi_loader.name)
self.custom_iso_args['meta_data']['efi_loader'] = efi_loader.name

# create initrd for install image
log.info('Creating install image boot image')
self._create_iso_install_kernel_and_initrd()

# the system image initrd is stored to allow kexec
self._copy_system_image_initrd_to_iso_image()

if self.firmware.efi_mode():
efi_loader = Temporary(
prefix='efi-loader.', path=self.target_dir
).new_file()
bootloader_config._create_embedded_fat_efi_image(efi_loader.name)
self.custom_iso_args['meta_data']['efi_loader'] = efi_loader.name

# create iso filesystem from media_dir
log.info('Creating ISO filesystem')
iso_image = FileSystemIsoFs(
Expand Down
42 changes: 31 additions & 11 deletions kiwi/builder/live.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from kiwi.runtime_config import RuntimeConfig
from kiwi.iso_tools.base import IsoToolsBase
from kiwi.xml_state import XMLState
from kiwi.command import Command

from kiwi.exceptions import KiwiLiveBootImageError

Expand All @@ -60,6 +61,9 @@ def __init__(
self, xml_state: XMLState, target_dir: str,
root_dir: str, custom_args: Dict = None
):
self.bootloader = xml_state.get_build_type_bootloader_name()
if self.bootloader != 'systemd_boot':
self.bootloader = 'grub2'
self.arch = Defaults.get_platform_name()
self.root_dir = root_dir
self.target_dir = target_dir
Expand All @@ -77,7 +81,9 @@ def __init__(
self.live_type = Defaults.get_default_live_iso_type()

self.boot_image = BootImageDracut(
xml_state, target_dir, self.root_dir
xml_state,
f'{root_dir}/boot' if self.bootloader == 'systemd_boot' else target_dir,
self.root_dir
)
self.firmware = FirmWare(
xml_state
Expand Down Expand Up @@ -146,7 +152,7 @@ def create(self) -> Result:
# for compat boot. The complete bootloader setup will be
# based on grub
bootloader_config = BootLoaderConfig.new(
'grub2', self.xml_state, root_dir=self.root_dir,
self.bootloader, self.xml_state, root_dir=self.root_dir,
boot_dir=self.media_dir.name, custom_args={
'grub_directory_name':
Defaults.get_grub_boot_directory_name(self.root_dir),
Expand Down Expand Up @@ -183,13 +189,6 @@ def create(self) -> Result:
working_directory=self.root_dir
)

if self.firmware.efi_mode():
efi_loader = Temporary(
prefix='efi-loader.', path=self.target_dir
).new_file()
bootloader_config._create_embedded_fat_efi_image(efi_loader.name)
custom_iso_args['meta_data']['efi_loader'] = efi_loader.name

# prepare dracut initrd call
self.boot_image.prepare()

Expand All @@ -210,10 +209,31 @@ def create(self) -> Result:
config_file=self.root_dir + '/etc/dracut.conf.d/02-livecd.conf'
)
self.boot_image.create_initrd(self.mbrid)
if self.bootloader == 'systemd_boot':
# make sure the initrd name follows the dracut naming conventions
boot_names = self.boot_image.get_boot_names()
if self.boot_image.initrd_filename:
Command.run(
[
'mv', self.boot_image.initrd_filename,
self.root_dir + ''.join(
['/boot/', boot_names.initrd_name]
)
]
)

# create EFI FAT image
if self.firmware.efi_mode():
efi_loader = Temporary(
prefix='efi-loader.', path=self.target_dir
).new_file()
bootloader_config._create_embedded_fat_efi_image(efi_loader.name)
custom_iso_args['meta_data']['efi_loader'] = efi_loader.name

# setup kernel file(s) and initrd in ISO boot layout
log.info('Setting up kernel file(s) and boot image in ISO boot layout')
self._setup_live_iso_kernel_and_initrd()
if self.bootloader != 'systemd_boot':
log.info('Setting up kernel file(s) and boot image in ISO boot layout')
self._setup_live_iso_kernel_and_initrd()

# calculate size and decide if we need UDF
if rootsize.accumulate_mbyte_file_sizes() > 4096:
Expand Down
Loading

0 comments on commit d6d57ea

Please sign in to comment.