nixos/test-driver: add option to force kvm use

This commit is contained in:
Michael Schneider
2026-04-15 12:14:00 +01:00
parent bffdf7c2a8
commit 5f5734db9d
7 changed files with 120 additions and 13 deletions

View File

@@ -2246,6 +2246,9 @@
"test-opt-passthru": [
"index.html#test-opt-passthru"
],
"test-opt-qemu.forceAccel": [
"index.html#test-opt-qemu.forceAccel"
],
"test-opt-qemu.package": [
"index.html#test-opt-qemu.package"
],

View File

@@ -24,30 +24,37 @@ rec {
else
throw "Unknown QEMU serial device for system '${stdenv.hostPlatform.system}'";
qemuBinary =
qemuPkg:
qemuBinary = qemuPkg: qemuBinaryWith { inherit qemuPkg; };
qemuBinaryWith =
{
qemuPkg,
forceAccel ? false,
}:
let
hostStdenv = qemuPkg.stdenv;
hostSystem = hostStdenv.system;
guestSystem = stdenv.hostPlatform.system;
accel = accelName: if forceAccel then accelName else "${accelName}:tcg";
linuxHostGuestMatrix = {
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=kvm:tcg -cpu max";
armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=kvm:tcg -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=kvm:tcg -cpu max";
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=${accel "kvm"} -cpu max";
armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=${accel "kvm"} -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=${accel "kvm"} -cpu max";
powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
riscv32-linux = "${qemuPkg}/bin/qemu-system-riscv32 -machine virt";
riscv64-linux = "${qemuPkg}/bin/qemu-system-riscv64 -machine virt";
x86_64-darwin = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=kvm:tcg -cpu max";
x86_64-darwin = "${qemuPkg}/bin/qemu-system-x86_64 -machine accel=${accel "kvm"} -cpu max";
};
otherHostGuestMatrix = {
aarch64-darwin = {
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=2,accel=hvf:tcg -cpu max";
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=2,accel=${accel "hvf"} -cpu max";
inherit (otherHostGuestMatrix.x86_64-darwin) x86_64-linux;
};
x86_64-darwin = {
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=hvf:tcg -cpu max";
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=${accel "hvf"} -cpu max";
};
};

View File

@@ -336,10 +336,22 @@ class Driver:
def start_all(self) -> None:
"""Start all machines"""
with self.logger.nested("start all VMs"):
errors: list[tuple[str, BaseException]] = []
def start_machine(machine: BaseMachine) -> None:
try:
machine.start()
except Exception as e:
errors.append((machine.name, e))
threads = []
for machine in self.machines:
# Create a thread for each machine's start method
t = threading.Thread(target=machine.start, name=f"start-{machine.name}")
t = threading.Thread(
target=start_machine,
args=(machine,),
name=f"start-{machine.name}",
)
threads.append(t)
t.start()
@@ -347,6 +359,12 @@ class Driver:
for t in threads:
t.join()
if errors:
messages = [f"{name}: {e}" for name, e in errors]
raise MachineError(
"Failed to start the following machines:\n" + "\n".join(messages)
)
def join_all(self) -> None:
"""Wait for all machines to shut down"""
with self.logger.nested("wait for all VMs to finish"):

View File

@@ -212,6 +212,7 @@ class QemuStartCommand:
),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
cwd=state_dir,
env=self.build_environment(state_dir, shared_dir),
@@ -1227,8 +1228,29 @@ class QemuMachine(BaseMachine):
self.shell_path,
allow_reboot,
)
self.monitor, _ = monitor_socket.accept()
self.shell, _ = shell_socket.accept()
def accept_or_fail(sock: socket.socket, name: str) -> socket.socket:
"""Accept a connection on a socket, polling the status to check
if the QEMU process is still alive. Without this, socket.accept()
would block forever if QEMU exits before connecting.
"""
assert self.process
while True:
readable, _, _ = select.select([sock], [], [], 1.0)
if readable:
conn, _ = sock.accept()
return conn
rc = self.process.poll()
if rc is not None:
output = ""
if self.process.stdout:
output = self.process.stdout.read().decode(errors="ignore")
raise MachineError(
f"QEMU process exited with code {rc} before connecting to {name} socket.\n{output}"
)
self.monitor = accept_or_fail(monitor_socket, "monitor")
self.shell = accept_or_fail(shell_socket, "shell")
self.qmp_client = QMPSession.from_path(self.qmp_path)
# Store last serial console lines for use

View File

@@ -159,6 +159,16 @@ in
defaultText = "hostPkgs.qemu_test";
};
qemu.forceAccel = mkOption {
description = ''
Whether to force the use of hardware-accelerated virtualisation.
When enabled, QEMU will not fall back to the slower software emulation
(TCG) and will instead error out if the accelerator is not available.
'';
type = types.bool;
default = false;
};
globalTimeout = mkOption {
description = ''
A global timeout for the complete test, expressed in seconds.

View File

@@ -71,7 +71,9 @@ let
config.nodeDefaults
{
key = "base-qemu";
virtualisation.qemu.package = testModuleArgs.config.qemu.package;
virtualisation.qemu = {
inherit (testModuleArgs.config.qemu) package forceAccel;
};
virtualisation.host.pkgs = hostPkgs;
}
testModuleArgs.config.extraBaseNodeModules

View File

@@ -306,8 +306,42 @@ let
(builtins.concatStringsSep "")
]}
${lib.optionalString cfg.qemu.forceAccel (
if hostPkgs.stdenv.hostPlatform.isLinux then
''
# Check for hardware-accelerated virtualisation support (KVM)
if [ ! -e /dev/kvm ]; then
echo "forceAccel is enabled but /dev/kvm does not exist." >&2
echo "Hardware-accelerated virtualisation (KVM) is not available on this system." >&2
exit 1
elif [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
echo "forceAccel is enabled but /dev/kvm is not accessible (permission denied)." >&2
echo "Check that the nix build user is in the 'kvm' group or that /dev/kvm has the correct permissions." >&2
exit 1
fi
''
else if hostPkgs.stdenv.hostPlatform.isDarwin then
''
# Check for hardware-accelerated virtualisation support (HVF)
if ! sysctl -n kern.hv_support 2>/dev/null | grep -q 1; then
echo "forceAccel is enabled but Hypervisor.framework is not available on this system." >&2
exit 1
fi
''
else
''
echo "forceAccel is enabled but no known accelerator is available for this platform." >&2
exit 1
''
)}
# Start QEMU.
exec ${qemu-common.qemuBinary qemu} \
exec ${
qemu-common.qemuBinaryWith {
qemuPkg = qemu;
forceAccel = cfg.qemu.forceAccel;
}
} \
-name ${config.system.name} \
-m ${toString config.virtualisation.memorySize} \
-smp ${toString config.virtualisation.cores} \
@@ -728,6 +762,17 @@ in
description = "QEMU package to use.";
};
forceAccel = mkOption {
type = types.bool;
default = false;
description = ''
Whether to force the use of hardware-accelerated virtualisation.
When enabled, QEMU will not fall back to the slower software
emulation (TCG) and will instead error out if the accelerator is not
available.
'';
};
options = mkOption {
type = types.listOf types.str;
default = [ ];