From 8aa4723dffaff85f4405315f9500a651310afb74 Mon Sep 17 00:00:00 2001 From: Lutz Reinhardt Date: Mon, 24 Aug 2026 14:54:56 +0000 Subject: [PATCH 1/3] feat: Add QEMU disk image parameter For running integration tests the binaries and their data have to be made available in the image. Mounting the data via an extra image is the easiest and fastest way. It has lesser system requirements compared to e.g. mounting and modifying a QCOW2 image. --- MODULE.bazel | 7 ++++ score/itf/plugins/qemu/__init__.py | 15 ++++++++ score/itf/plugins/qemu/qemu.py | 19 ++++++---- score/itf/plugins/qemu/qemu_process.py | 5 +++ score/itf/plugins/qemu/qemu_target.py | 1 + test/integration/BUILD | 50 ++++++++++++++++++++++++++ test/integration/test_qemu_disk.py | 48 +++++++++++++++++++++++++ test/resources/BUILD | 18 ++++++++++ test/resources/qemu_disk_content.txt | 1 + test/unit/test_qemu.py | 41 +++++++++++++++++++-- 10 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 test/integration/test_qemu_disk.py create mode 100644 test/resources/qemu_disk_content.txt diff --git a/MODULE.bazel b/MODULE.bazel index 67ff2d20..22e8f68e 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -210,6 +210,13 @@ use_repo( bazel_dep(name = "score_rules_imagefs", version = "0.0.3", dev_dependency = True) +# TODO: Switch back to a released version once the `ext4` rule is available in a tagged release. +git_override( + module_name = "score_rules_imagefs", + commit = "b1278766c01d9f4102c24e87b0ce9d035eaed832", + remote = "https://github.com/eclipse-score/rules_imagefs.git", +) + imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True) imagefs.toolchain( name = "score_qnx_x86_64_ifs_toolchain", diff --git a/score/itf/plugins/qemu/__init__.py b/score/itf/plugins/qemu/__init__.py index 071b2a14..ff3ff8de 100644 --- a/score/itf/plugins/qemu/__init__.py +++ b/score/itf/plugins/qemu/__init__.py @@ -80,6 +80,13 @@ def pytest_addoption(parser): help="Path to a QEMU disk image (qcow2, wic, or img). " "An ephemeral overlay is created so the original image is not modified.", ) + parser.addoption( + "--qemu-disk", + action="store", + default=None, + help="Path to an additional disk image to attach to the target as a second block device. " + "A qcow2 overlay is created so the original image is not modified.", + ) @pytest.fixture(scope="session") @@ -96,6 +103,7 @@ def config(request): qemu_kernel = request.config.getoption("qemu_kernel") qemu_image = request.config.getoption("qemu_image") rootfs = request.config.getoption("qemu_rootfs") + disk = request.config.getoption("qemu_disk") if qemu_image: logger.warning( @@ -108,6 +116,7 @@ def config(request): qemu_config=load_configuration(qemu_config), qemu_kernel=qemu_kernel, qemu_rootfs=rootfs, + qemu_disk=disk, ) @@ -115,14 +124,18 @@ def config(request): def target_init(config, request, dlt): logger.info(f"Starting tests on host: {socket.gethostname()}") overlay_path = None + disk_overlay_path = None if config.qemu_rootfs: overlay_path = _create_overlay(os.path.abspath(config.qemu_rootfs)) + if config.qemu_disk: + disk_overlay_path = _create_overlay(os.path.abspath(config.qemu_disk)) try: with qemu_target( Bunch( qemu_config=config.qemu_config, qemu_kernel=config.qemu_kernel, qemu_rootfs=overlay_path, + qemu_disk=disk_overlay_path, ) ) as qemu: pre_tests_phase(qemu) @@ -130,3 +143,5 @@ def target_init(config, request, dlt): finally: if overlay_path and os.path.exists(overlay_path): os.unlink(overlay_path) + if disk_overlay_path and os.path.exists(disk_overlay_path): + os.unlink(disk_overlay_path) diff --git a/score/itf/plugins/qemu/qemu.py b/score/itf/plugins/qemu/qemu.py index 4f06905c..e847b7a5 100644 --- a/score/itf/plugins/qemu/qemu.py +++ b/score/itf/plugins/qemu/qemu.py @@ -55,6 +55,7 @@ def __init__( port_forwarding, rootfs, kernel_cmdline, + disk, ): """Create a QEMU instance with the specified parameters. @@ -67,6 +68,7 @@ def __init__( :param list port_forwarding: List of port forwarding configurations. :param str rootfs: Optional path to a qcow2 disk image. :param str kernel_cmdline: Optional kernel command line string. + :param str disk: Optional path to an additional qcow2 disk image. """ if machine not in _SUPPORTED_MACHINES: raise ValueError("machine must be one of: " + ", ".join(sorted(_SUPPORTED_MACHINES))) @@ -78,6 +80,7 @@ def __init__( self.__port_forwarding = port_forwarding self.__rootfs = rootfs self.__kernel_cmdline = kernel_cmdline + self.__disk = disk self.__check_qemu_is_installed() @@ -147,10 +150,14 @@ def __build_qemu_command(self): + self.__network_devices_args() + self.__port_forwarding_args() + self.__kernel_args() - + self.__rootfs_args() + + ( + self.__disk_args(self.__disk, 0) + self.__disk_args(self.__rootfs, 1) + if self.__arch_config["architecture"] == "aarch64" + else self.__disk_args(self.__rootfs, 0) + self.__disk_args(self.__disk, 1) + ) ) - def __kernel_args(self): + def __kernel_args(self) -> list[str]: if not self.__path_to_kernel_image: return [] args = ["-kernel", self.__path_to_kernel_image] @@ -158,14 +165,14 @@ def __kernel_args(self): args.extend(["-append", self.__kernel_cmdline]) return args - def __rootfs_args(self): - if not self.__rootfs: + def __disk_args(self, disk: str, id: int) -> list[str]: + if not disk: return [] return [ "-device", - f"{self.__arch_config['block_device']},drive=vd0", + f"{self.__arch_config['block_device']},drive=vd{id}", "-drive", - f"if=none,format=qcow2,file={self.__rootfs},id=vd0", + f"if=none,format=qcow2,file={disk},id=vd{id}", ] def __network_devices_args(self): diff --git a/score/itf/plugins/qemu/qemu_process.py b/score/itf/plugins/qemu/qemu_process.py index 7f1221a0..765e0379 100644 --- a/score/itf/plugins/qemu/qemu_process.py +++ b/score/itf/plugins/qemu/qemu_process.py @@ -30,6 +30,7 @@ def __init__( machine, rootfs, kernel_cmdline, + disk, ): self._path_to_qemu_kernel_image = path_to_qemu_kernel_image self._available_ram = available_ram @@ -39,6 +40,7 @@ def __init__( self._machine = machine self._rootfs = rootfs self._kernel_cmdline = kernel_cmdline + self._disk = disk self._qemu = Qemu( self._path_to_qemu_kernel_image, self._available_ram, @@ -48,6 +50,7 @@ def __init__( machine=self._machine, rootfs=self._rootfs, kernel_cmdline=self._kernel_cmdline, + disk=self._disk, ) self._console = None @@ -65,6 +68,8 @@ def start(self): logger.info(f"Using QEMU kernel command line: {self._kernel_cmdline}") if self._rootfs is not None: logger.info(f"Using QEMU root filesystem image: {self._rootfs}") + if self._disk is not None: + logger.info(f"Using QEMU additional disk image: {self._disk}") subprocess_params = { "stdin": subprocess.PIPE, "stdout": subprocess.PIPE, diff --git a/score/itf/plugins/qemu/qemu_target.py b/score/itf/plugins/qemu/qemu_target.py index f5feb55d..43d845d2 100644 --- a/score/itf/plugins/qemu/qemu_target.py +++ b/score/itf/plugins/qemu/qemu_target.py @@ -293,6 +293,7 @@ def qemu_target(test_config): machine=test_config.qemu_config.qemu_machine, rootfs=test_config.qemu_rootfs, kernel_cmdline=test_config.qemu_config.qemu_kernel_cmdline, + disk=test_config.qemu_disk, ) else: process_ctx = nullcontext() diff --git a/test/integration/BUILD b/test/integration/BUILD index 66a190ec..7935b202 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -254,13 +254,63 @@ py_itf_test( ], ) +py_itf_test( + name = "test_ubuntu_disk", + srcs = [ + "test_qemu_disk.py", + ], + args = [ + "--qemu-config=$(location @os_images//ubuntu_x86_64:qemu_config)", + "--qemu-rootfs=$(location @os_images//ubuntu_x86_64:image)", + "--qemu-disk=$(location //test/resources:qemu_disk_image)", + ], + data = [ + "//test/resources:qemu_disk_image", + "@os_images//ubuntu_x86_64:image", + "@os_images//ubuntu_x86_64:qemu_config", + ], + plugins = [ + "//score/itf/plugins:qemu_plugin", + ], + tags = [ + "manual", + ], +) + +py_itf_test( + name = "test_ebclfsa_disk", + srcs = [ + "test_qemu_disk.py", + ], + args = [ + "--qemu-config=$(location @os_images//ebclfsa_aarch64:qemu_config)", + "--qemu-kernel=$(location @os_images//ebclfsa_aarch64:kernel)", + "--qemu-rootfs=$(location @os_images//ebclfsa_aarch64:image)", + "--qemu-disk=$(location //test/resources:qemu_disk_image)", + ], + data = [ + "//test/resources:qemu_disk_image", + "@os_images//ebclfsa_aarch64:image", + "@os_images//ebclfsa_aarch64:kernel", + "@os_images//ebclfsa_aarch64:qemu_config", + ], + plugins = [ + "//score/itf/plugins:qemu_plugin", + ], + tags = [ + "manual", + ], +) + test_suite( name = "linux_qemu_tests", tags = [ "manual", ], tests = [ + ":test_ebclfsa_disk", ":test_ebclfsa_ping", + ":test_ubuntu_disk", ":test_ubuntu_ping", ], ) diff --git a/test/integration/test_qemu_disk.py b/test/integration/test_qemu_disk.py new file mode 100644 index 00000000..dc7ed989 --- /dev/null +++ b/test/integration/test_qemu_disk.py @@ -0,0 +1,48 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""Verifies that a second disk, passed to the QEMU plugin via ``--qemu-disk``, +shows up in the guest, can be mounted and contains the expected content. +""" + +EXPECTED_DISK_CONTENT = "Hello from the QEMU disk image!\n" + +# The additional disk is attached as the second virtio block device. The +# rootfs occupies the first one, so the disk always shows up as /dev/vdb. +DISK_DEVICE = "/dev/vdb" +MOUNT_POINT = "/mnt/qemu_disk" + + +def test_disk_device_is_visible(target): + exit_code, _ = target.execute(f"test -b {DISK_DEVICE}") + assert exit_code == 0, f"Expected block device {DISK_DEVICE} to be present" + + +def test_disk_can_be_mounted_and_has_expected_content(target): + exit_code, _ = target.execute(f"mkdir -p {MOUNT_POINT} && mount {DISK_DEVICE} {MOUNT_POINT}") + assert exit_code == 0, "Mounting the additional disk failed" + try: + exit_code, output = target.execute(f"cat {MOUNT_POINT}/qemu_disk_content.txt") + assert exit_code == 0 + assert output.decode("utf-8") == EXPECTED_DISK_CONTENT + finally: + target.execute(f"umount {MOUNT_POINT}") + + +def test_disk_is_writable(target): + exit_code, _ = target.execute(f"mkdir -p {MOUNT_POINT} && mount {DISK_DEVICE} {MOUNT_POINT}") + assert exit_code == 0, "Mounting the additional disk failed" + try: + exit_code, _ = target.execute(f"touch {MOUNT_POINT}/should_be_writable") + assert exit_code == 0, "Writing to the disk should work" + finally: + target.execute(f"umount {MOUNT_POINT}") diff --git a/test/resources/BUILD b/test/resources/BUILD index f5700827..7667a04a 100644 --- a/test/resources/BUILD +++ b/test/resources/BUILD @@ -11,7 +11,9 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load") +load("@rules_pkg//pkg:mappings.bzl", "pkg_files") load("@rules_pkg//pkg:tar.bzl", "pkg_tar") +load("@score_rules_imagefs//rules/linux:ext4.bzl", "ext4") filegroup( name = "dlt_config", @@ -99,3 +101,19 @@ oci_load( #"@score_itf_examples//:__subpackages__", ], ) + +pkg_files( + name = "qemu_disk_files", + srcs = ["qemu_disk_content.txt"], + prefix = "", +) + +ext4( + name = "qemu_disk_image", + srcs = [":qemu_disk_files"], + out = "qemu_disk.ext4", + target_compatible_with = ["@platforms//os:linux"], + visibility = [ + "//test:__subpackages__", + ], +) diff --git a/test/resources/qemu_disk_content.txt b/test/resources/qemu_disk_content.txt new file mode 100644 index 00000000..04606245 --- /dev/null +++ b/test/resources/qemu_disk_content.txt @@ -0,0 +1 @@ +Hello from the QEMU disk image! diff --git a/test/unit/test_qemu.py b/test/unit/test_qemu.py index 9610a832..c6db8fe8 100644 --- a/test/unit/test_qemu.py +++ b/test/unit/test_qemu.py @@ -25,6 +25,7 @@ def _build_qemu( path_to_kernel_image=None, kernel_cmdline=None, rootfs=None, + disk=None, network_adapters=None, port_forwarding=None, ): @@ -44,6 +45,7 @@ def _build_qemu( port_forwarding=port_forwarding, rootfs=rootfs, kernel_cmdline=kernel_cmdline, + disk=disk, ) @@ -59,6 +61,7 @@ def test_invalid_machine_is_rejected(mocker): port_forwarding=[], rootfs=None, kernel_cmdline=None, + disk=None, ) @@ -99,7 +102,7 @@ def test_kernel_args_are_empty_without_kernel_image(mocker): def test_rootfs_args_include_arch_specific_block_device(mocker): qemu = _build_qemu(mocker, machine="virt-aarch64", rootfs="/tmp/rootfs.qcow2") - assert qemu._Qemu__rootfs_args() == [ + assert qemu._Qemu__disk_args("/tmp/rootfs.qcow2", 0) == [ "-device", "virtio-blk-device,drive=vd0", "-drive", @@ -110,7 +113,41 @@ def test_rootfs_args_include_arch_specific_block_device(mocker): def test_rootfs_args_are_empty_without_rootfs(mocker): qemu = _build_qemu(mocker) - assert qemu._Qemu__rootfs_args() == [] + assert qemu._Qemu__disk_args(None, 0) == [] + + +def test_disk_args_include_arch_specific_readonly_block_device(mocker): + qemu = _build_qemu(mocker, machine="virt-aarch64", disk="/tmp/disk.qcow2") + + assert qemu._Qemu__disk_args("/tmp/disk.qcow2", 1) == [ + "-device", + "virtio-blk-device,drive=vd1", + "-drive", + "if=none,format=qcow2,file=/tmp/disk.qcow2,id=vd1", + ] + + +def test_disk_args_are_empty_without_disk(mocker): + qemu = _build_qemu(mocker) + + assert qemu._Qemu__disk_args(None, 0) == [] + + +def test_rootfs_and_disk_can_be_combined(mocker): + qemu = _build_qemu(mocker, rootfs="/tmp/rootfs.qcow2", disk="/tmp/disk.qcow2") + + assert qemu._Qemu__disk_args("/tmp/rootfs.qcow2", 0) == [ + "-device", + "virtio-blk-pci,drive=vd0", + "-drive", + "if=none,format=qcow2,file=/tmp/rootfs.qcow2,id=vd0", + ] + assert qemu._Qemu__disk_args("/tmp/disk.qcow2", 1) == [ + "-device", + "virtio-blk-pci,drive=vd1", + "-drive", + "if=none,format=qcow2,file=/tmp/disk.qcow2,id=vd1", + ] def test_network_args_skip_loopback_and_use_machine_specific_device(mocker): From 531d73775163c7dd56a56554034f9bb2da5c6848 Mon Sep 17 00:00:00 2001 From: Lutz Reinhardt Date: Tue, 25 Aug 2026 09:06:32 +0000 Subject: [PATCH 2/3] document why AARCH64 needs reverse order --- score/itf/plugins/qemu/qemu.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/score/itf/plugins/qemu/qemu.py b/score/itf/plugins/qemu/qemu.py index e847b7a5..49cbc75b 100644 --- a/score/itf/plugins/qemu/qemu.py +++ b/score/itf/plugins/qemu/qemu.py @@ -25,6 +25,9 @@ "network_device": "virtio-net-pci", "machine": "pc", "block_device": "virtio-blk-pci", + # virtio-blk-pci is probed by the guest in the same order the devices are + # specified on the command line. + "block_device_order": "ascending", }, "virt-aarch64": { "architecture": "aarch64", @@ -32,6 +35,9 @@ "network_device": "virtio-net-device", "machine": "virt,virtualization=true,gic-version=3", "block_device": "virtio-blk-device", + # virtio-blk-device (virtio-mmio) is probed by the guest in the reverse order + # the devices are specified on the command line. + "block_device_order": "descending", }, } @@ -150,11 +156,7 @@ def __build_qemu_command(self): + self.__network_devices_args() + self.__port_forwarding_args() + self.__kernel_args() - + ( - self.__disk_args(self.__disk, 0) + self.__disk_args(self.__rootfs, 1) - if self.__arch_config["architecture"] == "aarch64" - else self.__disk_args(self.__rootfs, 0) + self.__disk_args(self.__disk, 1) - ) + + self.__disks_args() ) def __kernel_args(self) -> list[str]: @@ -165,6 +167,17 @@ def __kernel_args(self) -> list[str]: args.extend(["-append", self.__kernel_cmdline]) return args + def __disks_args(self) -> list[str]: + # Order the disks so that, regardless of the guest's probing order, the rootfs + # always ends up as the first block device (/dev/vda) in the guest. + disks = [self.__rootfs, self.__disk] + if self.__arch_config["block_device_order"] == "descending": + disks = list(reversed(disks)) + args = [] + for id, disk in enumerate(disks): + args += self.__disk_args(disk, id) + return args + def __disk_args(self, disk: str, id: int) -> list[str]: if not disk: return [] From c90bc56824848ece9d44ec9a4c759bba580b666e Mon Sep 17 00:00:00 2001 From: Lutz Reinhardt Date: Tue, 25 Aug 2026 09:53:43 +0000 Subject: [PATCH 3/3] fix copilot review findings --- score/itf/plugins/qemu/__init__.py | 8 ++++---- test/integration/BUILD | 4 ++++ test/unit/test_qemu.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/score/itf/plugins/qemu/__init__.py b/score/itf/plugins/qemu/__init__.py index ff3ff8de..47696f6a 100644 --- a/score/itf/plugins/qemu/__init__.py +++ b/score/itf/plugins/qemu/__init__.py @@ -125,11 +125,11 @@ def target_init(config, request, dlt): logger.info(f"Starting tests on host: {socket.gethostname()}") overlay_path = None disk_overlay_path = None - if config.qemu_rootfs: - overlay_path = _create_overlay(os.path.abspath(config.qemu_rootfs)) - if config.qemu_disk: - disk_overlay_path = _create_overlay(os.path.abspath(config.qemu_disk)) try: + if config.qemu_rootfs: + overlay_path = _create_overlay(os.path.abspath(config.qemu_rootfs)) + if config.qemu_disk: + disk_overlay_path = _create_overlay(os.path.abspath(config.qemu_disk)) with qemu_target( Bunch( qemu_config=config.qemu_config, diff --git a/test/integration/BUILD b/test/integration/BUILD index 7935b202..27a0257c 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -231,6 +231,7 @@ py_itf_test( tags = [ "manual", ], + target_compatible_with = ["@platforms//os:linux"], ) py_itf_test( @@ -252,6 +253,7 @@ py_itf_test( tags = [ "manual", ], + target_compatible_with = ["@platforms//os:linux"], ) py_itf_test( @@ -275,6 +277,7 @@ py_itf_test( tags = [ "manual", ], + target_compatible_with = ["@platforms//os:linux"], ) py_itf_test( @@ -300,6 +303,7 @@ py_itf_test( tags = [ "manual", ], + target_compatible_with = ["@platforms//os:linux"], ) test_suite( diff --git a/test/unit/test_qemu.py b/test/unit/test_qemu.py index c6db8fe8..9cc7747e 100644 --- a/test/unit/test_qemu.py +++ b/test/unit/test_qemu.py @@ -116,7 +116,7 @@ def test_rootfs_args_are_empty_without_rootfs(mocker): assert qemu._Qemu__disk_args(None, 0) == [] -def test_disk_args_include_arch_specific_readonly_block_device(mocker): +def test_disk_args_include_arch_specific_block_device(mocker): qemu = _build_qemu(mocker, machine="virt-aarch64", disk="/tmp/disk.qcow2") assert qemu._Qemu__disk_args("/tmp/disk.qcow2", 1) == [