diff --git a/MODULE.bazel b/MODULE.bazel index 67673cd..de3f182 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -30,12 +30,24 @@ bazel_dep(name = "rules_pkg", version = "1.0.1") bazel_dep(name = "score_itf", version = "0.5.0", dev_dependency = True) bazel_dep(name = "rules_python", version = "1.8.5", dev_dependency = True) +# TODO: Switch back to a released version once `--qemu-disk` is part of a tagged release. git_override( module_name = "score_itf", - commit = "53a7ba37bf304230fa846ec303c902ec12c8aaa2", + commit = "8aa4723dffaff85f4405315f9500a651310afb74", remote = "https://github.com/eclipse-score/itf.git", ) +# Filesystem image rules, used to build the additional disk image attached to the +# QEMU targets via `--qemu-disk`. +bazel_dep(name = "score_rules_imagefs", version = "0.0.3") + +# 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", +) + python = use_extension("@rules_python//python/extensions:python.bzl", "python", dev_dependency = True) python.defaults(python_version = "3.12") python.toolchain( diff --git a/QEMU_IMAGES.md b/QEMU_IMAGES.md index d3a22fd..ee6a1b2 100644 --- a/QEMU_IMAGES.md +++ b/QEMU_IMAGES.md @@ -26,6 +26,28 @@ aarch64 image. The image targets are tagged `manual`, since building them boots QEMU and needs the base images to be downloaded. +## Additional disk on /opt + +Both images mount an additional disk automatically on `/opt`. The ITF QEMU +plugin attaches such a disk when it is passed via `--qemu-disk`; it always shows +up as the second virtio block device `/dev/vdb`. The `fstab` entry uses `nofail`, +so the images keep booting when no additional disk is attached. + +`//tests:qemu_disk_image` builds an example ext4 disk with +[rules_imagefs](https://github.com/eclipse-score/rules_imagefs): + +```starlark +py_itf_test( + name = "my_test", + args = [ + "--qemu-config=$(location //ubuntu_x86_64:qemu_config)", + "--qemu-disk=$(location //tests:qemu_disk_image)", + "--qemu-rootfs=$(location //ubuntu_x86_64:image)", + ], + ... +) +``` + ## Build scripts All the image build scripts are located in the `//scripts` package. diff --git a/scripts/build_ebclfsa_aarch64_image.sh b/scripts/build_ebclfsa_aarch64_image.sh index 3bc9941..4d6f78f 100755 --- a/scripts/build_ebclfsa_aarch64_image.sh +++ b/scripts/build_ebclfsa_aarch64_image.sh @@ -97,6 +97,9 @@ done # Enable root login and empty passwords for integration testing, not for production use. sshpass -p linux ssh -o StrictHostKeyChecking=no -p 2222 root@localhost "printf 'PermitRootLogin yes\nPermitEmptyPasswords yes\nPasswordAuthentication yes\n' > /etc/ssh/sshd_config.d/99-integration-test.conf" +# Mount an additional disk (ITF "--qemu-disk") on /opt. "nofail" keeps the image +# bootable when no such disk is attached; crinit runs "mount -a" during boot. +sshpass -p linux ssh -o StrictHostKeyChecking=no -p 2222 root@localhost "printf '\n# Additional disk attached by the ITF QEMU plugin\n/dev/vdb\t/opt\tauto\tdefaults,nofail\t0\t0\n' >> /etc/fstab" sshpass -p linux ssh -o StrictHostKeyChecking=no -p 2222 root@localhost "sed -i 's/^root:[^:]*:/root::/' /etc/shadow && sync && crinit-ctl poweroff" || true if [[ -f "${WORKING_DIR}/qemu.pid" ]]; then diff --git a/tests/BUILD b/tests/BUILD index bec7bfc..7957513 100644 --- a/tests/BUILD +++ b/tests/BUILD @@ -11,7 +11,25 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@rules_pkg//pkg:mappings.bzl", "pkg_files") load("@score_itf//:defs.bzl", "py_itf_test") +load("@score_rules_imagefs//rules/linux:ext4.bzl", "ext4") + +pkg_files( + name = "qemu_disk_files", + srcs = ["qemu_disk_content.txt"], + prefix = "", +) + +# Additional disk attached to the QEMU targets via `--qemu-disk`. The images +# mount it on /opt automatically, see the `opt-mount` support in each image. +ext4( + name = "qemu_disk_image", + srcs = [":qemu_disk_files"], + out = "qemu_disk.ext4", + target_compatible_with = ["@platforms//os:linux"], + visibility = ["//visibility:public"], +) py_itf_test( name = "ubuntu_qemu_ping_test", @@ -46,3 +64,75 @@ py_itf_test( tags = ["qemu-integration"], target_compatible_with = ["@platforms//os:linux"], ) + +py_itf_test( + name = "ubuntu_qemu_opt_mount_test", + srcs = ["test_opt_mount.py"], + args = [ + "--qemu-config=$(location //ubuntu_x86_64:qemu_config)", + "--qemu-disk=$(location :qemu_disk_image)", + "--qemu-rootfs=$(location //ubuntu_x86_64:image)", + ], + data = [ + ":qemu_disk_image", + "//ubuntu_x86_64:image", + "//ubuntu_x86_64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], + tags = ["qemu-integration"], + target_compatible_with = ["@platforms//os:linux"], +) + +py_itf_test( + name = "ubuntu_qemu_no_disk_test", + srcs = ["test_no_disk.py"], + args = [ + "--qemu-config=$(location //ubuntu_x86_64:qemu_config)", + "--qemu-rootfs=$(location //ubuntu_x86_64:image)", + ], + data = [ + "//ubuntu_x86_64:image", + "//ubuntu_x86_64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], + tags = ["qemu-integration"], + target_compatible_with = ["@platforms//os:linux"], +) + +py_itf_test( + name = "ebclfsa_qemu_opt_mount_test", + srcs = ["test_opt_mount.py"], + args = [ + "--qemu-config=$(location //ebclfsa_aarch64:qemu_config)", + "--qemu-disk=$(location :qemu_disk_image)", + "--qemu-kernel=$(location //ebclfsa_aarch64:kernel)", + "--qemu-rootfs=$(location //ebclfsa_aarch64:image)", + ], + data = [ + ":qemu_disk_image", + "//ebclfsa_aarch64:image", + "//ebclfsa_aarch64:kernel", + "//ebclfsa_aarch64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], + tags = ["qemu-integration"], + target_compatible_with = ["@platforms//os:linux"], +) + +py_itf_test( + name = "ebclfsa_qemu_no_disk_test", + srcs = ["test_no_disk.py"], + args = [ + "--qemu-config=$(location //ebclfsa_aarch64:qemu_config)", + "--qemu-kernel=$(location //ebclfsa_aarch64:kernel)", + "--qemu-rootfs=$(location //ebclfsa_aarch64:image)", + ], + data = [ + "//ebclfsa_aarch64:image", + "//ebclfsa_aarch64:kernel", + "//ebclfsa_aarch64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], + tags = ["qemu-integration"], + target_compatible_with = ["@platforms//os:linux"], +) diff --git a/tests/qemu_disk_content.txt b/tests/qemu_disk_content.txt new file mode 100644 index 0000000..0460624 --- /dev/null +++ b/tests/qemu_disk_content.txt @@ -0,0 +1 @@ +Hello from the QEMU disk image! diff --git a/tests/test_no_disk.py b/tests/test_no_disk.py new file mode 100644 index 0000000..c99b6c3 --- /dev/null +++ b/tests/test_no_disk.py @@ -0,0 +1,36 @@ +# ******************************************************************************* +# 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 the automatic /opt mount does not break a target that is booted +without an additional disk. +""" + +MOUNT_POINT = "/opt" + + +def test_no_second_block_device(target): + exit_code, _ = target.execute("test -b /dev/vdb") + assert exit_code != 0, "No additional disk was attached, /dev/vdb must not exist" + + +def test_opt_is_not_a_mount_point(target): + exit_code, _ = target.execute(f"grep -q ' {MOUNT_POINT} ' /proc/mounts") + assert exit_code != 0, ( + f"{MOUNT_POINT} must not be mounted without an additional disk" + ) + + +def test_opt_is_usable(target): + exit_code, _ = target.execute( + f"test -d {MOUNT_POINT} && touch {MOUNT_POINT}/should_be_writable" + ) + assert exit_code == 0, f"{MOUNT_POINT} should still be a usable directory" diff --git a/tests/test_opt_mount.py b/tests/test_opt_mount.py new file mode 100644 index 0000000..4722de6 --- /dev/null +++ b/tests/test_opt_mount.py @@ -0,0 +1,45 @@ +# ******************************************************************************* +# 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 an additional disk passed via ``--qemu-disk`` is mounted on +/opt automatically, without any help from the test code. +""" + +# The additional disk is always attached as the second virtio block device, the +# root filesystem occupies the first one. +DISK_DEVICE = "/dev/vdb" +MOUNT_POINT = "/opt" +CONTENT_FILE = f"{MOUNT_POINT}/qemu_disk_content.txt" +EXPECTED_CONTENT = "Hello from the QEMU disk image!" + + +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_is_mounted_on_opt(target): + exit_code, _ = target.execute( + f"grep -q '^{DISK_DEVICE} {MOUNT_POINT} ' /proc/mounts" + ) + assert exit_code == 0, f"{DISK_DEVICE} is not mounted on {MOUNT_POINT}" + + +def test_opt_contains_disk_content(target): + exit_code, output = target.execute(f"cat {CONTENT_FILE}") + assert exit_code == 0, f"{CONTENT_FILE} is not readable" + assert output.decode("utf-8").strip() == EXPECTED_CONTENT + + +def test_opt_is_writable(target): + exit_code, _ = target.execute(f"touch {MOUNT_POINT}/should_be_writable") + assert exit_code == 0, f"{MOUNT_POINT} should be mounted read-write" diff --git a/ubuntu_x86_64/cloud-init/user-data b/ubuntu_x86_64/cloud-init/user-data index f83c4be..84e0b8f 100644 --- a/ubuntu_x86_64/cloud-init/user-data +++ b/ubuntu_x86_64/cloud-init/user-data @@ -96,6 +96,9 @@ runcmd: # Speed up boot: nothing must wait for a network that only comes up once # QEMU has attached the tap device. systemctl disable systemd-networkd-wait-online.service + # Mount an additional disk (ITF "--qemu-disk") on /opt. "nofail" keeps the + # image bootable when no such disk is attached. + echo '/dev/vdb /opt auto defaults,nofail,x-systemd.device-timeout=5s 0 0' >> /etc/fstab # From now on the image is fully configured; never run cloud-init again. touch /etc/cloud/cloud-init.disabled sync