Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove


imagefs = use_extension("@score_rules_imagefs//extensions:imagefs.bzl", "imagefs", dev_dependency = True)
imagefs.toolchain(
name = "score_qnx_x86_64_ifs_toolchain",
Expand Down
19 changes: 17 additions & 2 deletions score/itf/plugins/qemu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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(
Expand All @@ -108,25 +116,32 @@ def config(request):
qemu_config=load_configuration(qemu_config),
qemu_kernel=qemu_kernel,
qemu_rootfs=rootfs,
qemu_disk=disk,
)


@pytest.fixture(scope="session")
def target_init(config, request, dlt):
logger.info(f"Starting tests on host: {socket.gethostname()}")
overlay_path = None
if config.qemu_rootfs:
overlay_path = _create_overlay(os.path.abspath(config.qemu_rootfs))
disk_overlay_path = None
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,
qemu_kernel=config.qemu_kernel,
qemu_rootfs=overlay_path,
qemu_disk=disk_overlay_path,
)
) as qemu:
pre_tests_phase(qemu)
yield qemu
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)
32 changes: 26 additions & 6 deletions score/itf/plugins/qemu/qemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@
"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",
"cpu": "cortex-a53",
"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",
},
}

Expand All @@ -55,6 +61,7 @@ def __init__(
port_forwarding,
rootfs,
kernel_cmdline,
disk,
):
"""Create a QEMU instance with the specified parameters.

Expand All @@ -67,6 +74,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)))
Expand All @@ -78,6 +86,7 @@ def __init__(
self.__port_forwarding = port_forwarding
self.__rootfs = rootfs
self.__kernel_cmdline = kernel_cmdline
self.__disk = disk

self.__check_qemu_is_installed()

Expand Down Expand Up @@ -147,25 +156,36 @@ def __build_qemu_command(self):
+ self.__network_devices_args()
+ self.__port_forwarding_args()
+ self.__kernel_args()
+ self.__rootfs_args()
+ self.__disks_args()
)

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]
if self.__kernel_cmdline:
args.extend(["-append", self.__kernel_cmdline])
return args

def __rootfs_args(self):
if not self.__rootfs:
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 []
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):
Expand Down
5 changes: 5 additions & 0 deletions score/itf/plugins/qemu/qemu_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -48,6 +50,7 @@ def __init__(
machine=self._machine,
rootfs=self._rootfs,
kernel_cmdline=self._kernel_cmdline,
disk=self._disk,
)
self._console = None

Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions score/itf/plugins/qemu/qemu_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
54 changes: 54 additions & 0 deletions test/integration/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ py_itf_test(
tags = [
"manual",
],
target_compatible_with = ["@platforms//os:linux"],
)

py_itf_test(
Expand All @@ -252,6 +253,57 @@ py_itf_test(
tags = [
"manual",
],
target_compatible_with = ["@platforms//os:linux"],
)

py_itf_test(
Comment thread
lurtz marked this conversation as resolved.
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",
],
target_compatible_with = ["@platforms//os:linux"],
)

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",
],
target_compatible_with = ["@platforms//os:linux"],
)

test_suite(
Expand All @@ -260,7 +312,9 @@ test_suite(
"manual",
],
tests = [
":test_ebclfsa_disk",
":test_ebclfsa_ping",
":test_ubuntu_disk",
":test_ubuntu_ping",
],
)
Expand Down
48 changes: 48 additions & 0 deletions test/integration/test_qemu_disk.py
Original file line number Diff line number Diff line change
@@ -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}")
18 changes: 18 additions & 0 deletions test/resources/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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__",
],
)
1 change: 1 addition & 0 deletions test/resources/qemu_disk_content.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello from the QEMU disk image!
Loading
Loading