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
+ )