diff --git a/.github/workflows/itf.yml b/.github/workflows/itf.yml index c34888e..7e3d834 100644 --- a/.github/workflows/itf.yml +++ b/.github/workflows/itf.yml @@ -42,7 +42,7 @@ jobs: - name: Install qemu run: | sudo apt-get update - sudo apt-get install -y qemu-system-arm + sudo apt-get install -y qemu-system-arm qemu-system-x86 cloud-image-utils - name: Enable KVM group perms run: | sudo chmod 666 /dev/kvm diff --git a/MODULE.bazel b/MODULE.bazel index 011b211..11c3f09 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -234,3 +234,19 @@ http_file( sha256 = "9ca3891b27e4b7bbf6c519d8924283e89c03b62513a988f751a3ee3d10c293e4", urls = ["https://github.com/elektrobit/eb_corbos_toolkit/releases/download/v2.0.0-beta1/fastdev-ubuntu-ebclfsa-ebcl-qemuarm64.tar.gz"], ) + +############################################################################### +# +# Ubuntu minimal cloud image for x86_64 Linux QEMU integration tests +# +# Pinned to a dated release directory so the download stays reproducible. The +# minimal image is used instead of the full server image to reduce test times. +# The file uses the ".img" extension but is in fact a qcow2 image. +# +############################################################################### +http_file( + name = "ubuntu_noble_minimal_cloudimg_amd64", + downloaded_file_path = "ubuntu-24.04-minimal-cloudimg-amd64.img", + sha256 = "b3064efb500d71d6ccbe619b1716062b803e285116e040627b430aaee14cced6", + urls = ["https://cloud-images.ubuntu.com/minimal/releases/noble/release-20260801/ubuntu-24.04-minimal-cloudimg-amd64.img"], +) diff --git a/README.md b/README.md index 408a3fe..39d487b 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,47 @@ QEMU targets are configured using a JSON configuration file that specifies netwo } ``` +#### Linux QEMU images + +Running tests with Linux is supported: + +Ubuntu: + +```starlark +py_itf_test( + name = "test_qemu_ubuntu", + srcs = ["test_qemu_ubuntu.py"], + args = [ + "--qemu-rootfs=$(location //test/resources/ubuntu_x86_64:image)", + "--qemu-config=$(location //test/resources/ubuntu_x86_64:qemu_config)", + ], + data = [ + "//test/resources/ubuntu_x86_64:image", + "//test/resources/ubuntu_x86_64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], +) +``` + +Ebclfsa: + +```starlark +py_itf_test( + name = "test_qemu_ebclfsa", + srcs = ["test_qemu_ebclfsa.py"], + args = [ + "--qemu-rootfs=$(location //test/resources/ebclfsa_aarch64:image)", + "--qemu-kernel=$(location //test/resources/ebclfsa_aarch64:kernel)", + "--qemu-config=$(location //test/resources/ebclfsa_aarch64:qemu_config)", + ], + data = [ + "//test/resources/ebclfsa_aarch64:image", + "//test/resources/ebclfsa_aarch64:kernel", + "//test/resources/ebclfsa_aarch64:qemu_config", + ], + plugins = ["@score_itf//score/itf/plugins:qemu_plugin"], +) +``` ### Capability-Based Tests @@ -442,9 +483,9 @@ bazel test //test:test_docker \ ### QEMU Tests ```bash -# With pre-built QEMU image +# With a pre-built QEMU root filesystem image bazel test //test:test_qemu \ - --test_arg="--qemu-image=/path/to/kernel.img" + --test_arg="--qemu-rootfs=/path/to/rootfs.img" ``` ## QEMU Setup (Linux) diff --git a/test/integration/BUILD b/test/integration/BUILD index 7a43c8c..22e21e5 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -213,7 +213,7 @@ py_itf_test( py_itf_test( name = "test_ebclfsa_ping", srcs = [ - "test_ebclfsa_ping.py", + "test_ping.py", ], args = [ "--qemu-config=$(location //test/resources/ebclfsa_aarch64:qemu_config)", @@ -233,6 +233,27 @@ py_itf_test( ], ) +py_itf_test( + name = "test_ubuntu_ping", + srcs = [ + "test_ping.py", + ], + args = [ + "--qemu-config=$(location //test/resources/ubuntu_x86_64:qemu_config)", + "--qemu-rootfs=$(location //test/resources/ubuntu_x86_64:image)", + ], + data = [ + "//test/resources/ubuntu_x86_64:image", + "//test/resources/ubuntu_x86_64:qemu_config", + ], + plugins = [ + "//score/itf/plugins:qemu_plugin", + ], + tags = [ + "manual", + ], +) + test_suite( name = "linux_qemu_tests", tags = [ @@ -240,6 +261,7 @@ test_suite( ], tests = [ ":test_ebclfsa_ping", + ":test_ubuntu_ping", ], ) diff --git a/test/integration/test_ebclfsa_ping.py b/test/integration/test_ping.py similarity index 100% rename from test/integration/test_ebclfsa_ping.py rename to test/integration/test_ping.py diff --git a/test/resources/ubuntu_x86_64/BUILD b/test/resources/ubuntu_x86_64/BUILD new file mode 100644 index 0000000..7de5b38 --- /dev/null +++ b/test/resources/ubuntu_x86_64/BUILD @@ -0,0 +1,54 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +genrule( + name = "itf-image", + srcs = [ + "@ubuntu_noble_minimal_cloudimg_amd64//file", + "build_image.sh", + "render_user_data.py", + "cloud-init/user-data", + "cloud-init/meta-data", + "qemu_config.json", + ], + outs = ["ubuntu-x86_64-itf/ubuntu-24.04-minimal-cloudimg-amd64.img"], + cmd = """ + bash $(location build_image.sh) \ + $(RULEDIR) \ + $(location @ubuntu_noble_minimal_cloudimg_amd64//file) \ + $(RULEDIR)/ubuntu-x86_64-itf/ubuntu-24.04-minimal-cloudimg-amd64.img \ + $(location cloud-init/user-data) \ + $(location cloud-init/meta-data) \ + $(location qemu_config.json) \ + $(location render_user_data.py) + """, + tags = [ + "manual", + ], + visibility = ["//visibility:private"], +) + +filegroup( + name = "image", + srcs = [":ubuntu-x86_64-itf/ubuntu-24.04-minimal-cloudimg-amd64.img"], + tags = [ + "manual", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "qemu_config", + srcs = ["qemu_config.json"], + visibility = ["//:__subpackages__"], +) diff --git a/test/resources/ubuntu_x86_64/build_image.sh b/test/resources/ubuntu_x86_64/build_image.sh new file mode 100644 index 0000000..8c69bf3 --- /dev/null +++ b/test/resources/ubuntu_x86_64/build_image.sh @@ -0,0 +1,122 @@ +#!/bin/bash +# ******************************************************************************* +# 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 +# ******************************************************************************* +# +# Turns a stock Ubuntu cloud image into an image that can be driven by the score +# ITF QEMU plugin. +# +# The image is booted exactly once with a cloud-init NoCloud seed image attached. +# cloud-init applies the configuration from a rendered cloud-init/user-data +# derived from qemu_config.json, then powers the machine off again, so this +# script simply has to wait for QEMU to exit. +# +# Usage: build_image.sh + +set -euxo pipefail + +if [[ $# -ne 7 ]]; then + echo "Error: Expected 7 arguments (working directory, source image, target image, cloud-init user-data, cloud-init meta-data, qemu config, user-data renderer)" >&2 + exit 1 +fi + +WORKING_DIR="$1" +IMAGE_SOURCE="$2" +IMAGE_TARGET="$3" +USER_DATA="$4" +META_DATA="$5" +QEMU_CONFIG="$6" +USER_DATA_RENDERER="$7" + +# Size of the resulting disk. The stock cloud image ships a small virtual disk; +# growing it leaves room for artifacts uploaded by tests. +IMAGE_SIZE="8G" +# Upper bound for the customization boot. Generous because the build machine may +# not provide KVM, in which case QEMU falls back to TCG emulation. +BOOT_TIMEOUT_SECONDS=1200 + +for tool in qemu-system-x86_64 qemu-img cloud-localds python3; do + if ! command -v "${tool}" &>/dev/null; then + echo "Error: ${tool} is not installed. Please install it to proceed." >&2 + exit 1 + fi +done + +if [[ ! -f "${IMAGE_SOURCE}" ]]; then + echo "Error: Image source is not a file: ${IMAGE_SOURCE}" >&2 + exit 1 +fi + +for f in "${USER_DATA}" "${META_DATA}"; do + if [[ ! -f "${f}" ]]; then + echo "Error: cloud-init file does not exist: ${f}" >&2 + exit 1 + fi +done + +if [[ ! -f "${QEMU_CONFIG}" ]]; then + echo "Error: QEMU config does not exist: ${QEMU_CONFIG}" >&2 + exit 1 +fi + +if [[ ! -f "${USER_DATA_RENDERER}" ]]; then + echo "Error: user-data renderer does not exist: ${USER_DATA_RENDERER}" >&2 + exit 1 +fi + +mkdir -p "$(dirname "${IMAGE_TARGET}")" +rm -f "${IMAGE_TARGET}" +cp -L "${IMAGE_SOURCE}" "${IMAGE_TARGET}" + +# The image file must be writable, both for the customization boot below and for +# the qemu overlay handling in the ITF QEMU plugin. +chmod +w "${IMAGE_TARGET}" +qemu-img resize "${IMAGE_TARGET}" "${IMAGE_SIZE}" + +SEED_IMAGE="${WORKING_DIR}/score-itf-seed.img" +USER_DATA_RENDERED="${WORKING_DIR}/score-itf-user-data.yaml" +rm -f "${SEED_IMAGE}" + +# Keep cloud-init network settings in sync with runtime QEMU settings. +python3 "${USER_DATA_RENDERER}" "${QEMU_CONFIG}" "${USER_DATA}" "${USER_DATA_RENDERED}" + +cloud-localds "${SEED_IMAGE}" "${USER_DATA_RENDERED}" "${META_DATA}" + +QEMU_LOG="${WORKING_DIR}/qemu_customization.log" + +# -no-reboot makes QEMU exit instead of restarting, should cloud-init decide to +# reboot rather than power off. +timeout "${BOOT_TIMEOUT_SECONDS}" qemu-system-x86_64 \ + -accel kvm -accel tcg \ + -machine pc \ + -cpu Cascadelake-Server-v5 \ + -smp 2 \ + -m 2048 \ + -no-reboot \ + -device virtio-blk-pci,drive=vd0 -drive if=none,format=qcow2,file="${IMAGE_TARGET}",id=vd0 \ + -device virtio-blk-pci,drive=vd1 -drive if=none,format=raw,file="${SEED_IMAGE}",id=vd1,readonly=on \ + -netdev user,id=net0 \ + -device virtio-net-pci,netdev=net0 \ + -nographic \ + -serial mon:stdio "${QEMU_LOG}" 2>&1 + +rm -f "${SEED_IMAGE}" +rm -f "${USER_DATA_RENDERED}" + +# cloud-init reports failures on stdout but still powers the machine off, so the +# exit code of QEMU alone is not a sufficient success criterion. +if ! grep -q "score ITF image customization finished" "${QEMU_LOG}"; then + echo "Error: cloud-init did not complete the image customization." >&2 + echo "----- QEMU log -----" >&2 + cat "${QEMU_LOG}" >&2 + exit 1 +fi diff --git a/test/resources/ubuntu_x86_64/cloud-init/meta-data b/test/resources/ubuntu_x86_64/cloud-init/meta-data new file mode 100644 index 0000000..a004b71 --- /dev/null +++ b/test/resources/ubuntu_x86_64/cloud-init/meta-data @@ -0,0 +1,2 @@ +instance-id: score-itf-ubuntu-x86-64 +local-hostname: score-itf diff --git a/test/resources/ubuntu_x86_64/cloud-init/user-data b/test/resources/ubuntu_x86_64/cloud-init/user-data new file mode 100644 index 0000000..378691b --- /dev/null +++ b/test/resources/ubuntu_x86_64/cloud-init/user-data @@ -0,0 +1,105 @@ +#cloud-config +# ******************************************************************************* +# 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 +# ******************************************************************************* +# +# Turns a stock Ubuntu cloud image into an image usable by the score ITF QEMU +# plugin. This configuration is applied on a single throw-away boot performed by +# build_image.sh; afterwards cloud-init disables itself so that the resulting +# image is self-contained and boots straight into the configured state. +# +# The resulting image intentionally allows passwordless root SSH login. It is +# meant for integration testing only and must never be used in production. + +ssh_pwauth: true + +package_update: true +packages: + - iputils-ping + +write_files: + # Static address matching the tap0 device created by scripts/run_under_qemu.sh + # and the "networks" entry of qemu_config.json. + - path: /etc/netplan/99-score-itf.yaml + permissions: "0600" + owner: root:root + content: | + network: + version: 2 + renderer: networkd + ethernets: + itf0: + match: + name: "en*" + dhcp4: false + dhcp6: false + link-local: [] + optional: true + addresses: + - __SCORE_ITF_IP_ADDRESS__/16 + routes: + - to: default + via: __SCORE_ITF_GATEWAY__ + on-link: true + + # Prevent cloud-init from re-generating /etc/netplan/50-cloud-init.yaml. + - path: /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg + permissions: "0644" + owner: root:root + content: | + network: {config: disabled} + + # The ITF connects as root with an empty password (see QemuTarget.ssh). + - path: /etc/ssh/sshd_config.d/99-integration-test.conf + permissions: "0644" + owner: root:root + content: | + PermitRootLogin yes + PermitEmptyPasswords yes + PasswordAuthentication yes + KbdInteractiveAuthentication no + UseDNS no + +runcmd: + # A single fail-fast script: build_image.sh treats the absence of the marker + # printed at the very end as a build failure, so a partially customized image + # can never be produced. + - - bash + - -eux + - -c + - | + # Clear the root password so that password authentication succeeds with "". + passwd -d root + # pam_unix must accept empty passwords, otherwise sshd rejects the login + # even with PermitEmptyPasswords. + grep -q 'pam_unix\.so.*nullok' /etc/pam.d/common-auth || + sed -i 's/\(pam_unix\.so\)/\1 nullok/' /etc/pam.d/common-auth + grep -q 'pam_unix\.so.*nullok' /etc/pam.d/common-auth + # Drop the DHCP configuration generated by cloud-init, it would conflict + # with the static configuration written above. + rm -f /etc/netplan/50-cloud-init.yaml + netplan generate + # Make sure sshd is started on boot (Ubuntu 24.04 uses socket activation). + systemctl enable ssh.socket + # 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 + # From now on the image is fully configured; never run cloud-init again. + touch /etc/cloud/cloud-init.disabled + sync + echo "score ITF image customization finished" > /dev/console + +power_state: + mode: poweroff + message: score ITF image customization finished + timeout: 60 + condition: true diff --git a/test/resources/ubuntu_x86_64/qemu_config.json b/test/resources/ubuntu_x86_64/qemu_config.json new file mode 100644 index 0000000..e5e38ae --- /dev/null +++ b/test/resources/ubuntu_x86_64/qemu_config.json @@ -0,0 +1,13 @@ +{ + "networks": [ + { + "name": "tap0", + "ip_address": "169.254.158.190", + "gateway": "169.254.21.88" + } + ], + "ssh_port": 22, + "qemu_num_cores": 2, + "qemu_ram_size": "2G", + "qemu_machine": "pc-x86_64" +} diff --git a/test/resources/ubuntu_x86_64/render_user_data.py b/test/resources/ubuntu_x86_64/render_user_data.py new file mode 100644 index 0000000..dbe7264 --- /dev/null +++ b/test/resources/ubuntu_x86_64/render_user_data.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# 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 +# ******************************************************************************* + +"""Render cloud-init user-data using network values from qemu_config.json.""" + +from __future__ import annotations + +import ipaddress +import json +import sys + + +def main() -> None: + if len(sys.argv) != 4: + raise SystemExit("Error: Expected 3 arguments (qemu config, user-data template, rendered user-data)") + + qemu_config_path = sys.argv[1] + user_data_template_path = sys.argv[2] + user_data_rendered_path = sys.argv[3] + + with open(qemu_config_path, encoding="utf-8") as f: + qemu_config = json.load(f) + + networks = qemu_config.get("networks") + if not isinstance(networks, list) or not networks: + raise SystemExit("Error: qemu_config.json must provide at least one network") + + network = networks[0] + for key in ("ip_address", "gateway"): + if key not in network: + raise SystemExit(f"Error: missing '{key}' in first network entry") + + ip_address = str(network["ip_address"]) + gateway = str(network["gateway"]) + ipaddress.ip_address(ip_address) + ipaddress.ip_address(gateway) + + with open(user_data_template_path, encoding="utf-8") as f: + user_data = f.read() + + if "__SCORE_ITF_IP_ADDRESS__" not in user_data or "__SCORE_ITF_GATEWAY__" not in user_data: + raise SystemExit("Error: user-data template placeholders are missing") + + user_data = user_data.replace("__SCORE_ITF_IP_ADDRESS__", ip_address) + user_data = user_data.replace("__SCORE_ITF_GATEWAY__", gateway) + + with open(user_data_rendered_path, "w", encoding="utf-8") as f: + f.write(user_data) + + +if __name__ == "__main__": + main()