From 8d045bc62670da43bde740554d407cc57e43b4f2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 29 Jan 2024 17:48:26 +0100 Subject: [PATCH] test: add cross-arch build/boot test --- .github/workflows/tests.yml | 2 +- plans/all.fmf | 1 + test/test_build.py | 17 ++++++++++-- test/testcases.py | 9 ++++++ test/vm.py | 55 +++++++++++++++++++++++++------------ 5 files changed, 62 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index df7d1dd7a..4bb3eddbf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: - name: Install test dependencies run: | sudo apt update - sudo apt install -y podman python3-pytest python3-paramiko python3-boto3 flake8 qemu-system-x86 + sudo apt install -y podman python3-pytest python3-paramiko python3-boto3 flake8 qemu-efi-aarch64 qemu-system-x86 qemu-system-arm - name: Run tests env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} diff --git a/plans/all.fmf b/plans/all.fmf index 11065c36d..772161de3 100644 --- a/plans/all.fmf +++ b/plans/all.fmf @@ -12,6 +12,7 @@ provision: prepare: how: install package: + - edk2-aarch64 - podman - pytest - python3-boto3 diff --git a/test/test_build.py b/test/test_build.py index 96054ce63..01832f7b0 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -31,6 +31,7 @@ class ImageBuildResult(NamedTuple): img_type: str img_path: str + img_arch: str username: str password: str journal_output: str @@ -44,7 +45,13 @@ def image_type_fixture(tmpdir_factory, build_container, request, force_aws_uploa ImageBuildResult with the resulting image path and user/password """ # image_type is passed via special pytest parameter fixture - container_ref, image_type = request.param.split(",") + if request.param.count(",") == 2: + container_ref, image_type, target_arch = request.param.split(",") + elif request.param.count(",") == 1: + container_ref, image_type = request.param.split(",") + target_arch = None + else: + raise ValueError(f"cannot parse {request.param.count}") username = "test" password = "password" @@ -90,6 +97,9 @@ def image_type_fixture(tmpdir_factory, build_container, request, force_aws_uploa upload_args = [] creds_args = [] + target_arch_args = [] + if target_arch: + target_arch_args = ["--target-arch", target_arch] with tempfile.TemporaryDirectory() as tempdir: if image_type == "ami": @@ -120,6 +130,7 @@ def image_type_fixture(tmpdir_factory, build_container, request, force_aws_uploa "--config", "/output/config.json", "--type", image_type, *upload_args, + *target_arch_args, ]) journal_output = testutil.journal_after_cursor(cursor) metadata = {} @@ -132,7 +143,7 @@ def del_ami(): journal_log_path.write_text(journal_output, encoding="utf8") - return ImageBuildResult(image_type, generated_img, username, password, journal_output, metadata) + return ImageBuildResult(image_type, generated_img, target_arch, username, password, journal_output, metadata) def test_container_builds(build_container): @@ -150,7 +161,7 @@ def test_image_is_generated(image_type): @pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") @pytest.mark.parametrize("image_type", gen_testcases("direct-boot"), indirect=["image_type"]) def test_image_boots(image_type): - with QEMU(image_type.img_path) as test_vm: + with QEMU(image_type.img_path, arch=image_type.img_arch) as test_vm: exit_status, _ = test_vm.run("true", user=image_type.username, password=image_type.password) assert exit_status == 0 exit_status, output = test_vm.run("echo hello", user=image_type.username, password=image_type.password) diff --git a/test/testcases.py b/test/testcases.py index 9c4f5ef6d..ed68deab6 100644 --- a/test/testcases.py +++ b/test/testcases.py @@ -1,3 +1,4 @@ +import platform import os @@ -43,6 +44,14 @@ def gen_testcases(what): CONTAINERS_TO_TEST["centos"] + "," + DIRECT_BOOT_IMAGE_TYPES[2], CONTAINERS_TO_TEST["fedora"] + "," + DIRECT_BOOT_IMAGE_TYPES[0], ] + # do a cross arch test too + if platform.machine() == "x86_64": + # todo: add fedora:eln + test_cases.append( + f'{CONTAINERS_TO_TEST["centos"]},raw,arm64') + elif platform.machine() == "arm64": + # TODO: add arm64->x86_64 cross build test too + pass return test_cases elif what == "all": test_cases = [] diff --git a/test/vm.py b/test/vm.py index ad6401881..43c376363 100644 --- a/test/vm.py +++ b/test/vm.py @@ -1,6 +1,7 @@ import abc import os import pathlib +import platform import subprocess import sys import time @@ -93,10 +94,8 @@ def find_ovmf(): class QEMU(VM): MEM = "2000" - # TODO: support qemu-system-aarch64 too :) - QEMU = "qemu-system-x86_64" - def __init__(self, img, snapshot=True, cdrom=None): + def __init__(self, img, arch="", snapshot=True, cdrom=None): super().__init__() self._img = pathlib.Path(img) self._qmp_socket = self._img.with_suffix(".qemp-socket") @@ -104,22 +103,37 @@ def __init__(self, img, snapshot=True, cdrom=None): self._snapshot = snapshot self._cdrom = cdrom self._ssh_port = None + if not arch: + arch = platform.machine() + self._arch = arch def __del__(self): self.force_stop() - # XXX: move args to init() so that __enter__ can use them? - def start(self, wait_event="ssh", snapshot=True, use_ovmf=False): - if self.running(): - return - log_path = self._img.with_suffix(".serial-log") - self._ssh_port = get_free_port() - self._address = "localhost" - qemu_cmdline = [ - self.QEMU, "-enable-kvm", + def _gen_qemu_cmdline(self, snapshot, use_ovmf): + if self._arch in ("arm64", "aarch64"): + qemu_cmdline = [ + "qemu-system-aarch64", + "-machine", "virt", + "-cpu", "cortex-a57", + "-smp", "2", + "-bios", "/usr/share/AAVMF/AAVMF_CODE.fd", + ] + elif self._arch in ("amd64", "x86_64"): + qemu_cmdline = [ + "qemu-system-x86_64", + "-M", "accel=kvm", + # get "illegal instruction" inside the VM otherwise + "-cpu", "host", + ] + if use_ovmf: + qemu_cmdline.extend(["-bios", find_ovmf()]) + else: + raise ValueError(f"unsupported architecture {self._arch}") + + # common part + qemu_cmdline += [ "-m", self.MEM, - # get "illegal instruction" inside the VM otherwise - "-cpu", "host", "-serial", "stdio", "-monitor", "none", "-netdev", f"user,id=net.0,hostfwd=tcp::{self._ssh_port}-:22", @@ -128,18 +142,23 @@ def start(self, wait_event="ssh", snapshot=True, use_ovmf=False): ] if not os.environ.get("OSBUILD_TEST_QEMU_GUI"): qemu_cmdline.append("-nographic") - if use_ovmf: - qemu_cmdline.extend(["-bios", find_ovmf()]) if self._cdrom: qemu_cmdline.extend(["-cdrom", self._cdrom]) if snapshot: qemu_cmdline.append("-snapshot") qemu_cmdline.append(self._img) - self._log(f"vm starting, log available at {log_path}") + return qemu_cmdline + + # XXX: move args to init() so that __enter__ can use them? + def start(self, wait_event="ssh", snapshot=True, use_ovmf=False): + if self.running(): + return + self._ssh_port = get_free_port() + self._address = "localhost" # XXX: use systemd-run to ensure cleanup? self._qemu_p = subprocess.Popen( - qemu_cmdline, + self._gen_qemu_cmdline(snapshot, use_ovmf), stdout=sys.stdout, stderr=sys.stderr, )