feat: ARM ISO boot integration test, OVMF boot fixes
Some checks failed
CI/CD / typecheck (pull_request) Failing after 9s
CI/CD / test (pull_request) Failing after 10s
CI/CD / lint (pull_request) Failing after 22s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped

ARM integration test:
- arm-iso-provision.test.ts: aarch64 VM boots from bastion-generated ISO
- Uses QEMU aarch64 emulation (slow but validates the R1 scenario)
- Generous timeouts for emulated boot (15min discovery, 60min install)
- test-provision.sh updated: `sudo ./scripts/test-provision.sh arm`

VM boot fixes:
- setBootDisk() preserves UEFI loader/nvram when switching to disk boot
- /boot/efi mount gets nofail in fstab (prevents emergency mode in VMs)
- chronyd enable uses || true (fails in kickstart chroot)
- createIsoVm supports arch parameter for ARM VMs

Note: SSH-after-reboot in OVMF VMs still fails — OVMF doesn't respect
efibootmgr changes and loops PXE/HTTP Boot. Real hardware works fine.
The install flow itself (discovery → kickstart → complete) is validated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-03-27 00:26:12 +00:00
parent 46b017d77e
commit 7446d669c1
7 changed files with 423 additions and 18 deletions

View File

@@ -40,13 +40,17 @@ export function createPxeVm(config: PxeVmConfig): void {
const diskPath = join(IMAGE_DIR, `${config.name}.qcow2`);
run(`qemu-img create -f qcow2 "${diskPath}" ${config.diskSize}G`);
// OVMF firmware paths (Fedora)
const ovmfCode = arch === "aarch64"
? "/usr/share/edk2/aarch64/QEMU_EFI-pflash.raw"
: "/usr/share/edk2/ovmf/OVMF_CODE.fd";
if (!existsSync(ovmfCode)) {
throw new Error(`OVMF firmware not found at ${ovmfCode}. Install: sudo dnf install edk2-ovmf`);
// UEFI firmware paths (Fedora)
if (arch === "aarch64") {
const aavmf = "/usr/share/edk2/aarch64/QEMU_EFI.fd";
if (!existsSync(aavmf)) {
throw new Error(`AAVMF firmware not found at ${aavmf}. Install: sudo dnf install edk2-aarch64`);
}
} else {
const ovmf = "/usr/share/edk2/ovmf/OVMF_CODE.fd";
if (!existsSync(ovmf)) {
throw new Error(`OVMF firmware not found at ${ovmf}. Install: sudo dnf install edk2-ovmf`);
}
}
const virtInstallArgs = [
@@ -104,6 +108,31 @@ export function rebootPxeVm(name: string): void {
log(`PXE VM ${name} restarted`);
}
/** Change VM boot order to disk first (skip PXE on next boot). */
export function setBootDisk(name: string): void {
log(`Setting ${name} boot order to disk first`);
virsh("destroy", name);
spawnSync("sleep", ["2"]);
// Get current XML, replace boot dev='network' with boot dev='hd'
// This preserves UEFI loader/nvram settings (virt-xml --boot hd can break them)
const dumpXml = virsh("dumpxml", name);
if (dumpXml.status !== 0) throw new Error("Failed to dump VM XML");
let xml = dumpXml.stdout;
// Replace any <boot dev='...' /> entries with hd
xml = xml.replace(/<boot dev='[^']*'\/>/g, "<boot dev='hd'/>");
// If no boot dev entry, add one before </os>
if (!xml.includes("<boot dev=")) {
xml = xml.replace("</os>", " <boot dev='hd'/>\n </os>");
}
const xmlPath = `/tmp/${name}-bootfix.xml`;
const { writeFileSync: writeFs, unlinkSync: unlinkFs } = require("node:fs") as typeof import("node:fs");
writeFs(xmlPath, xml);
run(`virsh define "${xmlPath}"`);
try { unlinkFs(xmlPath); } catch { /* ignore */ }
virsh("start", name);
log(`${name} restarted with disk boot (UEFI preserved)`);
}
export interface IsoVmConfig {
name: string;
memory: number; // MB
@@ -111,13 +140,15 @@ export interface IsoVmConfig {
diskSize: number; // GB
network: string; // libvirt network name
isoPath: string; // path to boot ISO
arch?: "x86_64" | "aarch64";
}
/** Create a UEFI VM that boots from a CD-ROM ISO (not PXE). */
export function createIsoVm(config: IsoVmConfig): void {
destroyPxeVm(config.name);
log(`Creating ISO boot VM: ${config.name} (${config.memory}MB RAM, ${config.vcpus} vCPU, ${config.diskSize}GB disk)`);
const arch = config.arch ?? "x86_64";
log(`Creating ISO boot VM: ${config.name} (${arch}, ${config.memory}MB RAM, ${config.vcpus} vCPU, ${config.diskSize}GB disk)`);
// Create blank disk
const diskPath = join(IMAGE_DIR, `${config.name}.qcow2`);
@@ -140,7 +171,11 @@ export function createIsoVm(config: IsoVmConfig): void {
"--graphics=vnc,listen=127.0.0.1",
];
if (arch === "aarch64") {
virtInstallArgs.push("--arch=aarch64", "--machine=virt");
}
log(`Running: virt-install --name=${config.name} --boot=uefi,cdrom ...`);
run(virtInstallArgs.join(" "), { timeout: 30_000 });
run(virtInstallArgs.join(" "), { timeout: 60_000 });
log(`ISO boot VM ${config.name} created and booting from ISO`);
}