diff --git a/kiwi/bootloader/config/__init__.py b/kiwi/bootloader/config/__init__.py index db94efd8259..e0611953285 100644 --- a/kiwi/bootloader/config/__init__.py +++ b/kiwi/bootloader/config/__init__.py @@ -27,6 +27,7 @@ from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot from kiwi.bootloader.config.custom import BootLoaderConfigCustom from kiwi.bootloader.config.zipl import BootLoaderZipl + from kiwi.bootloader.config.iso_s390x import BootLoaderIsoS390x @overload @@ -63,16 +64,16 @@ def create_boot_loader_config( @overload def create_boot_loader_config( - *, name: str, xml_state: object, root_dir: str, + *, name: Literal["iso_s390x"], xml_state: object, root_dir: str, boot_dir: str = None, custom_args: Dict = None -) -> "Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderZipl]": +) -> "BootLoaderIsoS390x": ... # pragma: nocover def create_boot_loader_config( *, name: str, xml_state: object, root_dir: str, boot_dir: str = None, custom_args: Dict = None -) -> "Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderZipl, BootLoaderConfigCustom]": +) -> "Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderZipl, BootLoaderConfigCustom, BootLoaderIsoS390x]": if name in ("grub2", "grub2_s390x_emu"): from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 @@ -83,6 +84,9 @@ def create_boot_loader_config( if name == "zipl": from kiwi.bootloader.config.zipl import BootLoaderZipl return BootLoaderZipl(xml_state, root_dir, boot_dir, custom_args) + if name == "iso_s390x": + from kiwi.bootloader.config.iso_s390x import BootLoaderIsoS390x + return BootLoaderIsoS390x(xml_state, root_dir, boot_dir, custom_args) if name == "custom": from kiwi.bootloader.config.custom import BootLoaderConfigCustom return BootLoaderConfigCustom(xml_state, root_dir, boot_dir, custom_args) diff --git a/kiwi/bootloader/config/iso_s390x.py b/kiwi/bootloader/config/iso_s390x.py new file mode 100644 index 00000000000..0d69191c76c --- /dev/null +++ b/kiwi/bootloader/config/iso_s390x.py @@ -0,0 +1,243 @@ +# Copyright (c) 2026 SUSE Software Solutions Germany GmbH. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi. If not, see +# +import os +import logging +import struct +import shutil +from typing import Dict +from textwrap import dedent + +# project +from kiwi.system.identifier import SystemIdentifier +from kiwi.bootloader.config.base import BootLoaderConfigBase +from kiwi.path import Path + +log = logging.getLogger('kiwi') + + +class BootLoaderIsoS390x(BootLoaderConfigBase): + """ + **s390x iso bootloader configuration.** + """ + def post_init(self, custom_args: Dict = {}) -> None: + self.custom_args = custom_args + self.cmdline = self.get_boot_cmdline(None) or '' + self.config_files: Dict[str, str] = {} + self.lookup_path = '' + + def write_meta_data( + self, root_device: str = None, write_device: str = None, boot_options: str = '' + ) -> None: + self.cmdline = ' '.join( + [self.get_boot_cmdline(root_device, write_device), boot_options] + ).strip() + + def setup_disk_boot_images( + self, boot_uuid: str, efi_uuid: str = None, lookup_path: str = None + ) -> None: + pass + + def setup_disk_image_config( + self, boot_uuid: str = '', root_uuid: str = '', hypervisor: str = '', + kernel: str = '', initrd: str = '', boot_options: Dict[str, str] = {} + ) -> None: + pass + + def setup_install_boot_images( + self, mbrid: SystemIdentifier, lookup_path: str = '' + ) -> None: + log.info('Creating s390x install boot images') + self.lookup_path = lookup_path + + def setup_install_image_config( + self, mbrid: SystemIdentifier, hypervisor: str = 'xen.gz', + kernel: str = 'linux', initrd: str = 'initrd' + ) -> None: + log.info('Creating s390x install image config from template') + self._prepare_config_files() + + def setup_live_boot_images( + self, mbrid: SystemIdentifier, lookup_path: str = '' + ) -> None: + log.info('Creating s390x live boot images') + self.lookup_path = lookup_path + + def setup_live_image_config( + self, mbrid: SystemIdentifier, hypervisor: str = 'xen.gz', + kernel: str = 'linux', initrd: str = 'initrd' + ) -> None: + log.info('Creating s390x live image config file from template') + self._prepare_config_files() + + def setup_sysconfig_bootloader(self) -> None: + pass + + def _prepare_config_files(self) -> None: + relative_loader_path = self.get_boot_path('iso').lstrip('/') + + initrd_ofs_ofs = 0x0001040c + initrd_siz_ofs = 0x00010414 + initrd_ofs = 0x01000000 + parmfile_ofs = 0x00010480 + + # ensure it's never empty, add trailing space + parmfile_content = self.cmdline.strip() + " " + parmfile_hmc_content = f"{self.cmdline} console=ttyS1".strip() + " " + + suse_ins_content = dedent(f"""\ + * SUSE Linux for IBM z Systems Installation System + linux 0x00000000 + initrd.off 0x{initrd_ofs_ofs:08x} + initrd.siz 0x{initrd_siz_ofs:08x} + initrd 0x{initrd_ofs:08x} + parmfile 0x{parmfile_ofs:08x} + """) + + media_suse_ins_content = dedent(f"""\ + * SUSE Linux for IBM z Systems Installation System + {relative_loader_path}/linux 0x00000000 + {relative_loader_path}/initrd.off 0x{initrd_ofs_ofs:08x} + {relative_loader_path}/initrd.siz 0x{initrd_siz_ofs:08x} + {relative_loader_path}/initrd 0x{initrd_ofs:08x} + {relative_loader_path}/parmfile 0x{parmfile_ofs:08x} + """) + + media_susehmc_ins_content = dedent(f"""\ + * SUSE Linux for IBM z Systems Installation System via HMC + {relative_loader_path}/linux 0x00000000 + {relative_loader_path}/initrd.off 0x{initrd_ofs_ofs:08x} + {relative_loader_path}/initrd.siz 0x{initrd_siz_ofs:08x} + {relative_loader_path}/initrd 0x{initrd_ofs:08x} + {relative_loader_path}/parmfile.hmc 0x{parmfile_ofs:08x} + """) + + # Note: The following file has spaces at the end of the lines to make them exactly 80 chars wide. + sles_exec_content = dedent("""\ + /* REXX LOAD EXEC FOR SUSE LINUX S/390 VM GUESTS */ + /* LOADS SUSE LINUX S/390 FILES INTO READER */ + SAY '' + SAY 'LOADING SLES FILES INTO READER...' + 'CP CLOSE RDR' + 'PURGE RDR ALL' + 'SPOOL PUNCH * RDR' + 'PUNCH SLES LINUX A (NOH' + 'PUNCH SLES PARMFILE A (NOH' + 'PUNCH SLES INITRD A (NOH' + 'IPL 00C' + """) + + self.config_files[os.path.join(relative_loader_path, 'parmfile')] = parmfile_content + self.config_files[os.path.join(relative_loader_path, 'parmfile.hmc')] = parmfile_hmc_content + self.config_files[os.path.join(relative_loader_path, 'suse.ins')] = suse_ins_content + self.config_files['suse.ins'] = media_suse_ins_content + self.config_files['susehmc.ins'] = media_susehmc_ins_content + self.config_files[os.path.join(relative_loader_path, 'sles.exec')] = sles_exec_content + + def _create_s390x_boot_images(self, loader_path: str) -> None: + initrd_ofs_ofs = 0x0001040c + initrd_siz_ofs = 0x00010414 + initrd_ofs = 0x01000000 + parmfile_ofs = 0x00010480 + + kernel_dest = os.path.join(loader_path, 'linux') + initrd_dest = os.path.join(loader_path, 'initrd') + + if os.path.exists(kernel_dest) and os.path.exists(initrd_dest): + initrd_size = os.path.getsize(initrd_dest) + + initrd_off_data = struct.pack('>I', initrd_ofs) + with open(os.path.join(loader_path, 'initrd.off'), 'wb') as f: + f.write(initrd_off_data) + + initrd_siz_data = struct.pack('>I', initrd_size) + with open(os.path.join(loader_path, 'initrd.siz'), 'wb') as f: + f.write(initrd_siz_data) + + with open(kernel_dest, 'rb') as f: + cd_ikr_data = bytearray(f.read()) + + def write_at_offset(data: bytearray, offset: int, payload: bytes) -> None: + if len(data) < offset + len(payload): + data.extend(b'\x00' * (offset + len(payload) - len(data))) + data[offset:offset + len(payload)] = payload + + write_at_offset(cd_ikr_data, initrd_ofs_ofs, initrd_off_data) + write_at_offset(cd_ikr_data, initrd_siz_ofs, initrd_siz_data) + write_at_offset(cd_ikr_data, parmfile_ofs, b'\x00' * 512) + + parmfile_content = self.cmdline + write_at_offset(cd_ikr_data, parmfile_ofs, parmfile_content.encode('utf-8')) + + with open(initrd_dest, 'rb') as f: + initrd_bytes = f.read() + write_at_offset(cd_ikr_data, initrd_ofs, initrd_bytes) + + write_at_offset(cd_ikr_data, 4, b'\x80\x01\x00\x00') + + padding_size = -initrd_size & 0xfff + if padding_size > 0: + cd_ikr_data.extend(b'\x00' * padding_size) + + with open(os.path.join(loader_path, 'cd.ikr'), 'wb') as f: + f.write(cd_ikr_data) + log.info("Created s390x boot image: cd.ikr") + else: + log.info( + "Skipping cd.ikr creation because kernel and initrd are not yet present " + "in the boot loader directory." + ) + + with open(os.path.join(loader_path, 'zipl.map'), 'wb') as f: + f.write(b'\x00' * (4096 * 4)) + log.info("Created zipl.map") + + def setup_s390x_boot_images(self, kernel_file: str, initrd_file: str) -> None: + """ + Setup s390x boot images (linux, initrd, cd.ikr, zipl.map) + from the finalized kernel and initrd files. + """ + log.info('Setting up s390x boot images') + loader_path = os.path.join( + self.boot_dir, self.get_boot_path('iso').lstrip('/') + ) + Path.create(loader_path) + + kernel_dest = os.path.join(loader_path, 'linux') + initrd_dest = os.path.join(loader_path, 'initrd') + + shutil.copy(kernel_file, kernel_dest) + shutil.copy(initrd_file, initrd_dest) + log.info(f"Copied kernel to {kernel_dest} and initrd to {initrd_dest}") + + self._create_s390x_boot_images(loader_path) + + def write(self) -> None: + log.info('Writing s390x bootloader configuration') + loader_path = os.path.join( + self.boot_dir, self.get_boot_path('iso').lstrip('/') + ) + Path.create(loader_path) + + for filename, content in self.config_files.items(): + filepath = os.path.join(self.boot_dir, filename.lstrip('/')) + Path.create(os.path.dirname(filepath)) + with open(filepath, 'w') as f: + f.write(content) + log.info(f"Created configuration file {filepath}") + + self._create_s390x_boot_images(loader_path) diff --git a/kiwi/builder/install.py b/kiwi/builder/install.py index d15bb01a73b..0fa806f382d 100644 --- a/kiwi/builder/install.py +++ b/kiwi/builder/install.py @@ -71,8 +71,8 @@ def __init__( self.runtime_config = RuntimeConfig() self.arch = Defaults.get_platform_name() self.bootloader = xml_state.get_build_type_bootloader_name() - if self.bootloader != 'systemd_boot': - self.bootloader = 'grub2' + if self.bootloader not in ('custom', 'iso_s390x', 'systemd_boot'): + self.bootloader = 'grub2' if self.arch != 's390x' else 'iso_s390x' self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state @@ -163,7 +163,8 @@ def create_install_iso(self) -> None: 'efi_partition_table': self.firmware.get_partition_table_type(), 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr, 'ofw_mode': self.firmware.ofw_mode(), - 'legacy_bios_mode': self.firmware.legacy_bios_mode() + 'legacy_bios_mode': self.firmware.legacy_bios_mode(), + 'bootloader': self.bootloader } } diff --git a/kiwi/builder/live.py b/kiwi/builder/live.py index 4cb4916685a..d81a61b6800 100644 --- a/kiwi/builder/live.py +++ b/kiwi/builder/live.py @@ -32,6 +32,7 @@ from kiwi.bootloader.config import create_boot_loader_config from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot +from kiwi.bootloader.config.iso_s390x import BootLoaderIsoS390x from kiwi.bootloader.config.base import BootLoaderConfigBase from kiwi.filesystem import FileSystem from kiwi.filesystem.isofs import FileSystemIsoFs @@ -72,12 +73,12 @@ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Dict = None ): + self.arch = Defaults.get_platform_name() self.bootloader = xml_state.get_build_type_bootloader_name() - if self.bootloader != 'systemd_boot': - self.bootloader = 'grub2' + if self.bootloader not in ('custom', 'iso_s390x', 'systemd_boot'): + self.bootloader = 'grub2' if self.arch != 's390x' else 'iso_s390x' self.root_filesystem_verity_blocks = \ xml_state.build_type.get_verity_blocks() - self.arch = Defaults.get_platform_name() self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state @@ -135,7 +136,8 @@ def __init__( 'efi_mode': self.firmware.efi_mode(), 'efi_partition_table': self.firmware.get_partition_table_type(), 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr, - 'legacy_bios_mode': self.firmware.legacy_bios_mode() + 'legacy_bios_mode': self.firmware.legacy_bios_mode(), + 'bootloader': self.bootloader } } @@ -465,7 +467,9 @@ def create(self) -> Result: def create_live_iso_boot_images( self, - bootloader_config: Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot], + bootloader_config: Union[ + BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderIsoS390x + ], modules: List[str] = [] ) -> None: live_dracut_modules = Defaults.get_live_dracut_modules_from_flag( @@ -482,6 +486,15 @@ def create_live_iso_boot_images( self.boot_image.create_initrd(self.mbrid) # Clean up leftover dracut config file (which can break installs) os.unlink(self.root_dir + '/etc/dracut.conf.d/02-livecd.conf') + if self.arch == 's390x': + from kiwi.system.kernel import Kernel + kernel = Kernel(self.boot_image.boot_root_directory) + kernel_info = kernel.get_kernel() + if kernel_info and self.boot_image.initrd_filename: + bootloader_config.setup_s390x_boot_images( + kernel_file=kernel_info.filename, + initrd_file=self.boot_image.initrd_filename + ) if self.bootloader == 'systemd_boot': # make sure the initrd name follows the dracut # naming conventions diff --git a/kiwi/iso_tools/xorriso.py b/kiwi/iso_tools/xorriso.py index f532584aae6..24e8be3d169 100644 --- a/kiwi/iso_tools/xorriso.py +++ b/kiwi/iso_tools/xorriso.py @@ -74,7 +74,9 @@ def init_iso_creation_parameters( :param list custom_args: custom ISO meta data """ legacy_bios_mode = True + bootloader = None if custom_args: + bootloader = custom_args.get('bootloader') application_id = \ custom_args.get('application_id') or custom_args.get('mbr_id') if application_id: @@ -109,6 +111,15 @@ def init_iso_creation_parameters( '-compliance', 'no_emul_toc' ] + if bootloader == 'iso_s390x': + loader_file = '/'.join([self.boot_path, 'loader', 'cd.ikr']) + self.iso_loaders += [ + '-boot_image', 'any', f'bin_path={loader_file}', + '-boot_image', 'any', 'boot_info_table=off', + '-boot_image', 'any', 'load_size=512' + ] + return + if Defaults.is_x86_arch(self.arch) and legacy_bios_mode: mbr_file = os.sep.join( [ diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc index 3d228b5d690..9b18a0b0ba8 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -3030,7 +3030,7 @@ div { ## user which can be done by using the editbootinstall and ## editbootconfig custom scripts attribute name { - "grub2" | "grub2_s390x_emu" | "systemd_boot" | "custom" | "zipl" + "grub2" | "grub2_s390x_emu" | "systemd_boot" | "custom" | "zipl" | "iso_s390x" } >> sch:pattern [ id = "loader_name" is-a = "bootloader_image_type" sch:param [ name = "attr" value = "name" ] diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index 8591bc14738..fa6d2284d94 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -4568,6 +4568,7 @@ editbootconfig custom scripts systemd_boot custom zipl + iso_s390x diff --git a/test/unit/bootloader/config/init_test.py b/test/unit/bootloader/config/init_test.py index c3bbf3f5009..488b9985818 100644 --- a/test/unit/bootloader/config/init_test.py +++ b/test/unit/bootloader/config/init_test.py @@ -58,3 +58,13 @@ def test_bootloader_config_custom(self, mock_custom): mock_custom.assert_called_once_with( xml_state, 'root_dir', None, None ) + + @patch('kiwi.bootloader.config.iso_s390x.BootLoaderIsoS390x') + def test_bootloader_config_iso_s390x(self, mock_iso_s390x): + xml_state = Mock() + create_boot_loader_config( + name='iso_s390x', xml_state=xml_state, root_dir='root_dir' + ) + mock_iso_s390x.assert_called_once_with( + xml_state, 'root_dir', None, None + )