diff --git a/Schutzfile b/Schutzfile index 0c43e5f400..7b5b6f1377 100644 --- a/Schutzfile +++ b/Schutzfile @@ -15,7 +15,8 @@ "minimal-installer": "rhos-01/fedora-42", "network-installer": "rhos-01/fedora-42", "everything-network-installer": "rhos-01/fedora-42", - "server-network-installer": "rhos-01/fedora-42" + "server-network-installer": "rhos-01/fedora-42", + "pxe-tar-xz": "rhos-01/fedora-42" } } }, diff --git a/schutzbot/terraform b/schutzbot/terraform index ff2361a4eb..7d65c3a9d4 100644 --- a/schutzbot/terraform +++ b/schutzbot/terraform @@ -1 +1 @@ -c5ff0cf74a790e472f3f885ac640c82f0ec39a5e +3209ede9b21f1df5629afa8b087ae23a70f652a6 diff --git a/test/scripts/boot-image b/test/scripts/boot-image index 1df3a0f281..4b7d5e4048 100755 --- a/test/scripts/boot-image +++ b/test/scripts/boot-image @@ -134,6 +134,12 @@ class CannotRunQemuTest(Exception): self.skip_reason = skip_reason +class MissingBootImplementation(Exception): + def __init__(self, skip_reason): + super().__init__(skip_reason) + self.skip_reason = skip_reason + + def ensure_can_run_qemu_test(image_path, config_file): """ Check if the given image_path, config_file is capable of running a @@ -327,15 +333,18 @@ def boot_qemu_pxe(arch, pxe_tar_path): subprocess.check_call( "cat initrd.img rootfs.cpio > combined.img", shell=True, cwd=tmpdir) - # start an HTTP server to serve the rootfs.img + # Start an HTTP server to serve the rootfs.img and terminate it after the test. + # Explicitly terminate the HTTP server to avoid blocking on wait(), this cannot + # be done with a context manager for subprocesses. http_port = get_free_port() - with subprocess.Popen( + http_server = subprocess.Popen( # pylint: disable=consider-using-with ["python3", "-m", "http.server", f"{http_port}"], cwd=tmpdir, # prevent blocking output stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL - ): + ) + try: # test disk is unused for live OS test_disk_path = pathlib.Path(tmpdir) / "disk.img" with open(test_disk_path, "w", encoding="utf-8") as fp: @@ -372,6 +381,9 @@ def boot_qemu_pxe(arch, pxe_tar_path): # 2. modify vm.py to be able to talk directly to the serial console # and then run commands directly via that vm.force_stop() + finally: + http_server.terminate() + http_server.wait() def cmd_boot_aws(arch, image_name, privkey, pubkey, image_path, script_cmd): @@ -529,6 +541,10 @@ def main(): arch = build_info["arch"] image_type = build_info["image-type"] + if not testlib.can_boot_test(image_type, arch): + print(f"{image_type} boot tests are not supported yet") + return + print(f"Testing image at {image_path}") bib_image_id = "" # Keep test/scripts/imagetestlib.py:CAN_BOOT_TEST in sync as it @@ -574,8 +590,7 @@ def main(): boot_wsl(distro, arch, image_path, build_config_path) case _: # skip - print(f"{image_type} boot tests are not supported yet") - return + raise MissingBootImplementation(f"{arch} {image_type} is missing a boot implementation.") print("✅ Marking boot successful") # amend build info with boot success diff --git a/test/scripts/imgtestlib.py b/test/scripts/imgtestlib.py index 6d8d8d446e..050f6bba0a 100644 --- a/test/scripts/imgtestlib.py +++ b/test/scripts/imgtestlib.py @@ -292,7 +292,16 @@ def read_manifests(path): return manifests +def can_boot_test(image_type, arch): + return image_type in CAN_BOOT_TEST.get("*", []) + CAN_BOOT_TEST.get(arch, []) + + def check_for_build(manifest_fname, build_info_dir, errors): + """ + Checks if a manifest was built (and optionally booted) successfully. + + This function returns True if the image needs to be built. + """ build_info_path = os.path.join(build_info_dir, "info.json") # rebuild if matching build info is not found if not os.path.exists(build_info_path): @@ -323,7 +332,7 @@ def check_for_build(manifest_fname, build_info_dir, errors): print(" No PR/branch info available") image_type = dl_config["image-type"] - if image_type not in CAN_BOOT_TEST.get("*", []) + CAN_BOOT_TEST.get(dl_config["arch"], []): + if not can_boot_test(image_type, dl_config["arch"]): print(f" Boot testing for {image_type} is not yet supported") return False diff --git a/vmtest/vm.py b/vmtest/vm.py index fcae780dda..6bbdc94b0a 100644 --- a/vmtest/vm.py +++ b/vmtest/vm.py @@ -53,7 +53,7 @@ def _log(self, msg): sys.stdout.write(msg.rstrip("\n") + "\n") def wait_ssh_ready(self): - wait_ssh_ready(self._address, self._ssh_port, sleep=1, max_wait_sec=600) + wait_ssh_ready(self._address, self._ssh_port, sleep=1, max_wait_sec=1800) @abc.abstractmethod def force_stop(self): @@ -170,6 +170,12 @@ def __del__(self): self.force_stop() shutil.rmtree(self._tmpdir) + def _num_cores(self): + """ + Return the number of CPU cores available on the system. + """ + return os.cpu_count() or 1 + def _gen_qemu_cmdline(self, snapshot, use_ovmf): virtio_scsi_hd = [ "-device", "virtio-scsi-pci,id=scsi", @@ -181,15 +187,18 @@ def _gen_qemu_cmdline(self, snapshot, use_ovmf): "qemu-system-aarch64", "-machine", "virt", "-cpu", "cortex-a57", - "-smp", "2", + "-accel", "tcg,thread=multi", + "-smp", str(self._num_cores()), "-bios", "/usr/share/AAVMF/AAVMF_CODE.fd", ] + virtio_scsi_hd 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", + "-M", "q35,accel=kvm", + # RHEL 10 requires x86_64-v3 pass it to avoid "illegal instruction", but + # do not use "host" because some modern features are not supported by some + # distributions when running locally on very recent laptops. + "-cpu", "Haswell-v4", ] + virtio_scsi_hd if use_ovmf: qemu_cmdline.extend(["-bios", find_ovmf()]) @@ -197,13 +206,13 @@ def _gen_qemu_cmdline(self, snapshot, use_ovmf): qemu_cmdline = [ "qemu-system-ppc64", "-machine", "pseries", - "-smp", "2", + "-smp", str(self._num_cores()), ] + virtio_scsi_hd elif self._arch == "s390x": qemu_cmdline = [ "qemu-system-s390x", "-machine", "s390-ccw-virtio", - "-smp", "2", + "-smp", str(self._num_cores()), # sepcial disk setup "-device", "virtio-blk,drive=disk0,bootindex=1", ] @@ -211,26 +220,38 @@ def _gen_qemu_cmdline(self, snapshot, use_ovmf): else: raise ValueError(f"unsupported architecture {self._arch}") + if self._img.suffix == ".qcow2": + img_format = "qcow2" + elif self._img.suffix == ".img": + img_format = "raw" + else: + raise ValueError(f"Unsupported image extension: {self._img}. Must be .qcow2 or .img") + # common part qemu_cmdline += [ - "-m", self._memory, + "-m", str(self._memory), "-serial", "stdio", "-monitor", "none", + # make sure SSH key generation during boot does not block due to lack of entropy + "-object", "rng-random,filename=/dev/urandom,id=rng0", + "-device", "virtio-rng-pci,rng=rng0", + # network "-device", f"{virtio_net_device},netdev=net.0,id=net.0", "-netdev", f"user,id=net.0,hostfwd=tcp::{self._ssh_port}-:22", "-qmp", f"unix:{self._qmp_socket},server,nowait", # boot - "-drive", f"file={self._img},if=none,id=disk0,format=qcow2", + "-drive", f"file={self._img},if=none,id=disk0,cache=unsafe,format={img_format}", ] if not os.environ.get("OSBUILD_TEST_QEMU_GUI"): qemu_cmdline.append("-nographic") if self._cdrom: - qemu_cmdline.extend(["-cdrom", self._cdrom]) + qemu_cmdline.extend(["-cdrom", str(self._cdrom)]) if snapshot: qemu_cmdline.append("-snapshot") if self._extra_args: - qemu_cmdline.extend(self._extra_args) - qemu_cmdline.append(self._img) + qemu_cmdline.extend(str(arg) for arg in self._extra_args) + + print("QEMU: " + " ".join(qemu_cmdline)) return qemu_cmdline # XXX: move args to init() so that __enter__ can use them?