diff --git a/bastion/package.json b/bastion/package.json index e6de7a7..1e2a673 100644 --- a/bastion/package.json +++ b/bastion/package.json @@ -20,7 +20,9 @@ "test:integration:pxe": "vitest run -c tests/integration/vitest.config.ts -t 'PXE boot'", "test:integration:pxe:host": "sudo -E $(which npx) vitest run -c tests/integration/vitest.config.ts -t 'PXE boot'", "test:integration:iso": "vitest run -c tests/integration/vitest.config.ts -t 'ISO boot'", - "test:integration:iso:host": "sudo -E $(which npx) vitest run -c tests/integration/vitest.config.ts -t 'ISO boot'" + "test:integration:iso:host": "sudo -E $(which npx) vitest run -c tests/integration/vitest.config.ts -t 'ISO boot'", + "test:integration:arm-iso": "vitest run -c tests/integration/vitest.config.ts -t 'ARM ISO'", + "test:integration:arm-iso:host": "sudo -E $(which npx) vitest run -c tests/integration/vitest.config.ts -t 'ARM ISO'" }, "engines": { "node": ">=20.0.0", diff --git a/bastion/scripts/test-provision.sh b/bastion/scripts/test-provision.sh index e6d8e3f..4bc20c6 100755 --- a/bastion/scripts/test-provision.sh +++ b/bastion/scripts/test-provision.sh @@ -2,13 +2,16 @@ # Run PXE and/or ISO boot integration tests. # # Usage: -# sudo ./scripts/test-provision.sh # run both PXE + ISO tests +# sudo ./scripts/test-provision.sh # run PXE + ISO (x86_64) # sudo ./scripts/test-provision.sh pxe # PXE only -# sudo ./scripts/test-provision.sh iso # ISO only +# sudo ./scripts/test-provision.sh iso # ISO only (x86_64) +# sudo ./scripts/test-provision.sh arm # ARM ISO boot (emulated, SLOW ~60min) +# sudo ./scripts/test-provision.sh all # all tests including ARM # # Prerequisites: # libvirtd, OVMF (edk2-ovmf), iPXE (ipxe-bootimgs-x86), # dnsmasq, xorriso, mtools, virt-install, qemu-img +# ARM: qemu-system-aarch64, edk2-aarch64 set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -112,12 +115,30 @@ case "$MODE" in iso) run_test "ISO boot" "ISO boot" || FAILED=1 ;; - both|all) + arm|arm-iso) + if ! command -v qemu-system-aarch64 &>/dev/null; then + echo -e "${RED}qemu-system-aarch64 not found.${RESET} Install: sudo dnf install qemu-system-aarch64 edk2-aarch64" + exit 1 + fi + echo -e "${YELLOW}ARM emulation is ~10x slower than native. Expect 30-60 minutes.${RESET}" + run_test "ARM ISO boot" "ARM ISO" || FAILED=1 + ;; + both) run_test "PXE boot" "PXE boot" || FAILED=1 run_test "ISO boot" "ISO boot" || FAILED=1 ;; + all) + run_test "PXE boot" "PXE boot" || FAILED=1 + run_test "ISO boot" "ISO boot" || FAILED=1 + if command -v qemu-system-aarch64 &>/dev/null; then + echo -e "${YELLOW}ARM emulation is ~10x slower than native.${RESET}" + run_test "ARM ISO boot" "ARM ISO" || FAILED=1 + else + echo -e "${YELLOW}Skipping ARM test (qemu-system-aarch64 not installed)${RESET}" + fi + ;; *) - echo "Usage: $0 [pxe|iso|both]" + echo "Usage: $0 [pxe|iso|arm|both|all]" exit 1 ;; esac diff --git a/bastion/src/bastion/src/templates/install.ks.ts b/bastion/src/bastion/src/templates/install.ks.ts index 3aa6269..967aba7 100644 --- a/bastion/src/bastion/src/templates/install.ks.ts +++ b/bastion/src/bastion/src/templates/install.ks.ts @@ -409,6 +409,9 @@ hostnamectl set-hostname ${fqdn} # -- tmpfs for /tmp -- echo "tmpfs /tmp tmpfs defaults,noatime,nosuid,nodev,size=4G 0 0" >> /etc/fstab +# Make /boot/efi mount non-fatal (prevents emergency mode if EFI partition isn't found) +sed -i '/\\/boot\\/efi/ s/defaults/defaults,nofail/' /etc/fstab + ${isVanilla ? `# -- vanilla role: skip k3s kernel/sysctl/firewall setup -- bastion_progress "post-install" "vanilla role -- skipping k3s setup" # -- Enable chronyd for time sync -- diff --git a/bastion/tests/integration/arm-iso-provision.test.ts b/bastion/tests/integration/arm-iso-provision.test.ts new file mode 100644 index 0000000..8e549b4 --- /dev/null +++ b/bastion/tests/integration/arm-iso-provision.test.ts @@ -0,0 +1,334 @@ +// Integration test: ARM (aarch64) boot ISO provisioning. +// +// Simulates the MinisForum R1 scenario: ARM machine without PXE support, +// booting from the bastion-generated ISO via QEMU aarch64 emulation. +// +// IMPORTANT: This runs under emulation (not KVM), so it's SLOW (~30-60 min). +// The ISO contains an embedded aarch64 kernel+initrd for the fallback path. +// +// Prerequisites: +// - qemu-system-aarch64 installed (sudo dnf install qemu-system-aarch64) +// - edk2-aarch64 installed (sudo dnf install edk2-aarch64) +// - xorriso, mtools for ISO generation +// - sudo access, libvirtd running +// +// Run: sudo pnpm run test:integration:arm-iso + +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { readFileSync, existsSync, mkdirSync, rmSync, copyFileSync, symlinkSync, writeFileSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { join } from "node:path"; +import { homedir, tmpdir } from "node:os"; +import { log, waitForSsh } from "./helpers/libvirt.js"; +import { ensurePxeNetwork, destroyPxeNetwork, PXE_NETWORK_NAME, PXE_GATEWAY, PXE_SUBNET } from "./helpers/pxe-network.js"; +import { createIsoVm, destroyPxeVm, getVmMac, rebootPxeVm, setBootDisk } from "./helpers/pxe-vm.js"; +import { sshExec } from "./helpers/ssh.js"; + +const VM_NAME = "lab-arm-iso-test"; +const VM_MEMORY = 4096; +const VM_VCPUS = 2; +const VM_DISK_GB = 250; // LVM layout needs ~204GB, QCOW2 is sparse +const HTTP_PORT = 8097; // different from x86 tests +const SSH_USER = "michal"; +const BASTION_IP = PXE_GATEWAY; +const DHCP_RANGE_START = `${PXE_SUBNET}.100`; +const DHCP_RANGE_END = `${PXE_SUBNET}.200`; + +// ARM emulation is SLOW — generous timeouts +const DISCOVERY_TIMEOUT_MS = 15 * 60_000; // 15 min for emulated boot + discovery +const INSTALL_TIMEOUT_MS = 60 * 60_000; // 60 min for emulated Fedora install +const SSH_TIMEOUT_MS = 15 * 60_000; // 15 min for emulated reboot + SSH + +function findSshKey(): { pubKey: string; keyPath: string } { + const homes = [homedir()]; + const sudoUser = process.env["SUDO_USER"]; + if (sudoUser) homes.push(join("/home", sudoUser)); + if (process.env["SSH_KEY_PATH"]) { + const keyPath = process.env["SSH_KEY_PATH"]; + const pubPath = `${keyPath}.pub`; + if (existsSync(keyPath) && existsSync(pubPath)) { + return { pubKey: readFileSync(pubPath, "utf-8").trim(), keyPath }; + } + } + for (const home of homes) { + for (const name of ["id_ed25519", "id_ecdsa", "id_rsa"]) { + const keyPath = join(home, ".ssh", name); + const pubPath = `${keyPath}.pub`; + if (existsSync(keyPath) && existsSync(pubPath)) { + return { pubKey: readFileSync(pubPath, "utf-8").trim(), keyPath }; + } + } + } + throw new Error("No SSH key found"); +} + +function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +async function pollApi( + url: string, + check: (data: T) => boolean, + timeoutMs: number, + intervalMs = 10000, +): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const res = await fetch(url); + if (res.ok) { + const data = (await res.json()) as T; + if (check(data)) return data; + } + } catch { /* bastion not ready */ } + await sleep(intervalMs); + } + throw new Error(`Timeout after ${timeoutMs}ms polling ${url}`); +} + +describe("ARM ISO boot provisioning", () => { + let bastionApp: { close: () => Promise }; + let testDir: string; + let vmMac: string; + let vmIp: string; + let sshKeyPath: string; + + beforeAll(async () => { + // Check ARM prerequisites + if (!existsSync("/usr/bin/qemu-system-aarch64")) { + throw new Error("qemu-system-aarch64 not installed. Run: sudo dnf install qemu-system-aarch64"); + } + if (!existsSync("/usr/share/edk2/aarch64/QEMU_EFI.fd")) { + throw new Error("AAVMF not installed. Run: sudo dnf install edk2-aarch64"); + } + + const { pubKey, keyPath } = findSshKey(); + sshKeyPath = keyPath; + + // 1. Network + log("Setting up PXE test network (for ARM ISO test)..."); + ensurePxeNetwork(); + + // 2. Bastion dirs + testDir = join(tmpdir(), `lab-arm-iso-test-${Date.now()}`); + mkdirSync(testDir, { recursive: true }); + mkdirSync(join(testDir, "tftp"), { recursive: true }); + mkdirSync(join(testDir, "http"), { recursive: true }); + mkdirSync(join(testDir, "logs"), { recursive: true }); + + // 3. Start bastion with ISO generation (needs both x86_64 and aarch64 artifacts) + log("Starting bastion with ARM boot ISO generation..."); + + const { createApp } = await import("../../src/bastion/src/server.js"); + const { loadConfig } = await import("../../src/bastion/src/config.js"); + const { generateDnsmasqConf, startDnsmasq } = await import("../../src/bastion/src/services/dnsmasq.js"); + const { generateDiscoverKickstart } = await import("../../src/bastion/src/services/kickstart-generator.js"); + const { renderBootIpxe } = await import("../../src/bastion/src/templates/boot.ipxe.js"); + const { ensureBootIso } = await import("../../src/bastion/src/routes/boot-iso.js"); + + const config = loadConfig({ + bastionDir: testDir, + httpPort: HTTP_PORT, + iface: "virbr-pxe", + serverIp: BASTION_IP, + network: `${PXE_SUBNET}.0`, + gateway: BASTION_IP, + dhcpMode: "full", + dhcpRangeStart: DHCP_RANGE_START, + dhcpRangeEnd: DHCP_RANGE_END, + domain: "arm-test.local", + sshKeys: [pubKey], + adminUser: SSH_USER, + }); + + // iPXE for TFTP (x86_64 — still needed for dnsmasq config) + const ipxeSrc = "/usr/share/ipxe/ipxe-snponly-x86_64.efi"; + if (existsSync(ipxeSrc)) { + copyFileSync(ipxeSrc, join(config.tftpDir, "ipxe.efi")); + } + + // Fedora kernel + initrd for BOTH architectures + const cacheDir = "/var/lib/libvirt/images/lab-pxe-cache"; + execSync(`mkdir -p "${cacheDir}"`, { stdio: "pipe" }); + + // x86_64 (for the bastion HTTP serving) + for (const [arch, prefix] of [["x86_64", ""], ["aarch64", "aarch64-"]] as const) { + const mirror = `https://download.fedoraproject.org/pub/fedora/linux/releases/${config.fedoraVersion}/Everything/${arch}/os`; + const kernel = join(cacheDir, `vmlinuz-${arch}`); + const initrd = join(cacheDir, `initrd-${arch}.img`); + + if (!existsSync(kernel)) { + log(`Downloading Fedora ${config.fedoraVersion} ${arch} kernel...`); + execSync(`curl -# -L -f -o "${kernel}" "${mirror}/images/pxeboot/vmlinuz"`, { stdio: "inherit", timeout: 300_000 }); + } else { + log(`Fedora ${arch} kernel cached`); + } + if (!existsSync(initrd)) { + log(`Downloading Fedora ${config.fedoraVersion} ${arch} initrd...`); + execSync(`curl -# -L -f -o "${initrd}" "${mirror}/images/pxeboot/initrd.img"`, { stdio: "inherit", timeout: 300_000 }); + } else { + log(`Fedora ${arch} initrd cached`); + } + + // x86_64 artifacts go to httpDir for normal PXE serving + if (arch === "x86_64") { + copyFileSync(kernel, join(config.httpDir, "vmlinuz")); + copyFileSync(initrd, join(config.httpDir, "initrd.img")); + } + } + + try { symlinkSync(join(config.tftpDir, "ipxe.efi"), join(config.httpDir, "ipxe.efi")); } catch { /* exists */ } + + // Generate boot scripts + const discoverKs = generateDiscoverKickstart(config); + writeFileSync(join(config.httpDir, "discover.ks"), discoverKs); + const bootIpxe = renderBootIpxe({ serverIp: config.serverIp, httpPort: config.httpPort }); + writeFileSync(join(config.httpDir, "boot.ipxe"), bootIpxe); + + // Generate the boot ISO — includes both x86_64 and aarch64 payloads + log("Generating boot ISO with ARM support..."); + ensureBootIso(config); + + const isoPath = join(config.httpDir, "boot.iso"); + if (!existsSync(isoPath)) { + throw new Error("Boot ISO was not generated"); + } + const { statSync } = await import("node:fs"); + const isoSize = statSync(isoPath).size; + log(`Boot ISO generated: ${(isoSize / 1024 / 1024).toFixed(1)}MB`); + + // dnsmasq + HTTP server + generateDnsmasqConf(config); + const { app } = createApp(config); + bastionApp = app; + await app.listen({ port: config.httpPort, host: "0.0.0.0" }); + log(`Bastion HTTP listening on :${HTTP_PORT}`); + + void startDnsmasq(config); + await sleep(1000); + + // 4. Create ARM VM booting from ISO + log("Creating ARM ISO boot VM (aarch64 emulation — this will be SLOW)..."); + createIsoVm({ + name: VM_NAME, + memory: VM_MEMORY, + vcpus: VM_VCPUS, + diskSize: VM_DISK_GB, + network: PXE_NETWORK_NAME, + isoPath, + arch: "aarch64", + }); + + const mac = getVmMac(VM_NAME); + if (!mac) throw new Error("Could not determine VM MAC address"); + vmMac = mac; + log(`ARM VM MAC: ${vmMac}`); + + // 5. Wait for discovery (ARM emulation is slow) + log("Waiting for ARM VM to boot ISO -> iPXE -> DHCP -> bastion -> discover..."); + log("(ARM emulation is ~10x slower than native — be patient)"); + type MachinesResponse = { discovered: Record }; + await pollApi( + `http://${BASTION_IP}:${HTTP_PORT}/api/machines`, + (data) => vmMac in data.discovered, + DISCOVERY_TIMEOUT_MS, + 15_000, + ); + log("ARM VM discovered via ISO boot!"); + + // 6. Queue install + log("Queueing ARM machine for install..."); + await fetch(`http://${BASTION_IP}:${HTTP_PORT}/api/install`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mac: vmMac, hostname: VM_NAME, disk: "", role: "vanilla" }), + }); + + // 7. Reboot for install + log("Waiting for discovery reboot..."); + await sleep(30_000); // ARM is slow + rebootPxeVm(VM_NAME); + + // 8. Wait for install (ARM emulation makes this very slow) + log("Waiting for ARM install (emulated — expect 30-60 minutes)..."); + type LogsResponse = { status: string; progress: string; ip?: string }; + const finalState = await pollApi( + `http://${BASTION_IP}:${HTTP_PORT}/api/logs/${encodeURIComponent(vmMac)}`, + (data) => data.status === "installed" || data.progress === "error", + INSTALL_TIMEOUT_MS, + 30_000, + ); + + if (finalState.progress === "error") { + const logsRes = await fetch(`http://${BASTION_IP}:${HTTP_PORT}/api/logs/${encodeURIComponent(vmMac)}`); + const logs = await logsRes.json(); + log(`ARM INSTALL FAILED. State: ${JSON.stringify(logs, null, 2)}`); + throw new Error("ARM install failed — check logs above"); + } + + vmIp = finalState.ip ?? ""; + log(`ARM install complete! VM IP: ${vmIp}`); + + // 9. Switch boot to disk + log("Switching ARM VM boot order to disk..."); + await sleep(15_000); + setBootDisk(VM_NAME); + + // 10. Wait for SSH (ARM reboot is slow) + log("Waiting for SSH on ARM VM..."); + await waitForSsh(vmIp, SSH_USER, SSH_TIMEOUT_MS, sshKeyPath); + log("ARM ISO boot provision test setup complete."); + + }, DISCOVERY_TIMEOUT_MS + INSTALL_TIMEOUT_MS + SSH_TIMEOUT_MS + 300_000); + + afterAll(async () => { + log("Cleaning up ARM test..."); + if (bastionApp) await bastionApp.close().catch(() => {}); + const { stopDnsmasq } = await import("../../src/bastion/src/services/dnsmasq.js"); + stopDnsmasq(); + destroyPxeVm(VM_NAME); + destroyPxeNetwork(); + if (testDir) rmSync(testDir, { recursive: true, force: true }); + }); + + it("ARM machine was discovered and installed", async () => { + const res = await fetch(`http://${BASTION_IP}:${HTTP_PORT}/api/machines`); + const data = (await res.json()) as { installed: Record }; + expect(data.installed[vmMac]).toBeDefined(); + expect(data.installed[vmMac].hostname).toBe(VM_NAME); + }); + + it("architecture is aarch64", async () => { + const result = sshExec(vmIp, SSH_USER, "uname -m", { keyPath: sshKeyPath, timeout: 30_000 }); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe("aarch64"); + }); + + it("SSH works", () => { + const result = sshExec(vmIp, SSH_USER, "whoami", { keyPath: sshKeyPath, timeout: 30_000 }); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toBe(SSH_USER); + }); + + it("hostname is correct", () => { + const result = sshExec(vmIp, SSH_USER, "hostname -f", { keyPath: sshKeyPath, timeout: 30_000 }); + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toContain(VM_NAME); + }); + + it("provisioning metadata exists", () => { + const result = sshExec(vmIp, SSH_USER, "cat /etc/lab-provisioned", { keyPath: sshKeyPath, timeout: 30_000 }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(`hostname: ${VM_NAME}`); + expect(result.stdout).toContain("role: vanilla"); + }); + + it("LVM layout is correct", () => { + const result = sshExec(vmIp, SSH_USER, "sudo lvs labvg --noheadings -o lv_name", { keyPath: sshKeyPath, timeout: 30_000 }); + expect(result.exitCode).toBe(0); + const lvs = result.stdout.trim().split("\n").map((l: string) => l.trim()); + for (const expected of ["root", "var", "varlog", "swap", "home", "srv"]) { + expect(lvs).toContain(expected); + } + }); +}); diff --git a/bastion/tests/integration/helpers/pxe-vm.ts b/bastion/tests/integration/helpers/pxe-vm.ts index c330e9a..ac4a354 100644 --- a/bastion/tests/integration/helpers/pxe-vm.ts +++ b/bastion/tests/integration/helpers/pxe-vm.ts @@ -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 entries with hd + xml = xml.replace(//g, ""); + // If no boot dev entry, add one before + if (!xml.includes("", " \n "); + } + 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`); } diff --git a/bastion/tests/integration/iso-provision.test.ts b/bastion/tests/integration/iso-provision.test.ts index 53a9ff9..0250965 100644 --- a/bastion/tests/integration/iso-provision.test.ts +++ b/bastion/tests/integration/iso-provision.test.ts @@ -18,7 +18,7 @@ import { homedir, tmpdir } from "node:os"; import { mkdirSync, rmSync } from "node:fs"; import { log, waitForSsh } from "./helpers/libvirt.js"; import { ensurePxeNetwork, destroyPxeNetwork, PXE_NETWORK_NAME, PXE_GATEWAY, PXE_SUBNET } from "./helpers/pxe-network.js"; -import { createIsoVm, destroyPxeVm, getVmMac, rebootPxeVm } from "./helpers/pxe-vm.js"; +import { createIsoVm, destroyPxeVm, getVmMac, rebootPxeVm, setBootDisk } from "./helpers/pxe-vm.js"; import { sshExec } from "./helpers/ssh.js"; const VM_NAME = "lab-iso-test"; @@ -245,7 +245,12 @@ describe("ISO boot provisioning", () => { vmIp = finalState.ip ?? ""; log(`Install complete! VM IP: ${vmIp}`); - // 9. Wait for SSH + // 9. Switch boot to disk + log("Switching VM boot order to disk..."); + await sleep(10_000); + setBootDisk(VM_NAME); + + // 10. Wait for SSH log("Waiting for SSH..."); await waitForSsh(vmIp, SSH_USER, SSH_TIMEOUT_MS, sshKeyPath); log("ISO boot provision test setup complete."); diff --git a/bastion/tests/integration/pxe-provision.test.ts b/bastion/tests/integration/pxe-provision.test.ts index 5a23c05..074d9de 100644 --- a/bastion/tests/integration/pxe-provision.test.ts +++ b/bastion/tests/integration/pxe-provision.test.ts @@ -24,7 +24,7 @@ import { join } from "node:path"; import { homedir, tmpdir } from "node:os"; import { log, waitForSsh } from "./helpers/libvirt.js"; import { ensurePxeNetwork, destroyPxeNetwork, PXE_NETWORK_NAME, PXE_GATEWAY, PXE_SUBNET } from "./helpers/pxe-network.js"; -import { createPxeVm, destroyPxeVm, getVmMac, rebootPxeVm } from "./helpers/pxe-vm.js"; +import { createPxeVm, destroyPxeVm, getVmMac, rebootPxeVm, setBootDisk } from "./helpers/pxe-vm.js"; import { sshExec } from "./helpers/ssh.js"; // --- Test constants --- @@ -267,7 +267,12 @@ describe("PXE boot provisioning", () => { vmIp = finalState.ip ?? ""; log(`Install complete! VM IP: ${vmIp}`); - // 9. Wait for SSH + // 9. Switch VM boot to disk (OVMF PXE/HTTP Boot loop prevents reaching installed OS) + log("Switching VM boot order to disk..."); + await sleep(10_000); // Let kickstart reboot settle + setBootDisk(VM_NAME); + + // 10. Wait for SSH log("Waiting for SSH access..."); await waitForSsh(vmIp, SSH_USER, SSH_TIMEOUT_MS, sshKeyPath);