1 { system ? builtins.currentSystem,
3 pkgs ? import ../.. { inherit system config; },
10 with import ../lib/testing-python.nix { inherit system pkgs; };
14 testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
15 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
18 #!${pkgs.runtimeShell} -xe
19 export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}"
21 mkdir -p /run/dbus /var
23 cat > /etc/passwd <<EOF
24 root:x:0:0::/root:/bin/false
25 messagebus:x:1:1::/run/dbus:/bin/false
27 cat > /etc/group <<EOF
32 "${pkgs.dbus}/bin/dbus-daemon" --fork \
33 --config-file="${pkgs.dbus}/share/dbus-1/system.conf"
35 ${guestAdditions}/bin/VBoxService
36 ${(attrs.vmScript or (const "")) pkgs}
39 while [ ! -e /mnt-root/shutdown ]; do
42 [ $i -le 120 ] || fail
45 rm -f /mnt-root/boot-done /mnt-root/shutdown
49 "console=tty0" "console=ttyS0" "ignore_loglevel"
50 "boot.trace" "panic=1" "boot.panic_on_fail"
51 "init=${pkgs.writeScript "mini-init.sh" miniInit}"
59 virtualisation.virtualbox.guest.enable = true;
61 boot.initrd.kernelModules = [
63 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
66 boot.initrd.extraUtilsCommands = ''
67 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
68 copy_bin_and_libs "${pkgs.util-linux}/bin/unshare"
69 ${(attrs.extraUtilsCommands or (const "")) pkgs}
72 boot.initrd.postMountCommands = ''
73 touch /mnt-root/boot-done
76 unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
77 mount -t vboxsf nixstore /nix/store
83 system.requiredKernelConfig = with config.lib.kernelConfig; [
84 (isYes "SERIAL_8250_CONSOLE")
88 networking.usePredictableInterfaceNames = false;
91 mkLog = logfile: tag: let
92 rotated = map (i: "${logfile}.${toString i}") (range 1 9);
93 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
94 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
95 in if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";
97 testVM = vmName: vmScript: let
98 cfg = (import ../lib/eval-config.nix {
99 system = if use64bitGuest then "x86_64-linux" else "i686-linux";
101 ../modules/profiles/minimal.nix
102 (testVMConfig vmName vmScript)
105 in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
108 diskImage="$(pwd)/qimage"
109 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
113 echo "creating VirtualBox disk image..."
114 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
115 "$diskImage" "$out/disk.vdi"
118 buildInputs = [ pkgs.util-linux pkgs.perl ];
120 ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
121 ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
122 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
123 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
126 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
127 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
129 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
131 cat > /mnt/grub/grub.cfg <<GRUB
133 linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
140 createVM = name: attrs: let
141 mkFlags = concatStringsSep " ";
143 sharePath = "/home/alice/vboxshare-${name}";
145 createFlags = mkFlags [
146 "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
152 "--uartmode1 client /run/virtualbox-log-${name}.sock"
155 ] ++ (attrs.vmFlags or []));
157 controllerFlags = mkFlags [
164 diskFlags = mkFlags [
170 "--medium ${testVM name attrs}/disk.vdi"
173 sharedFlags = mkFlags [
175 "--hostpath ${sharePath}"
178 nixstoreFlags = mkFlags [
180 "--hostpath /nix/store"
185 systemd.sockets."vboxtestlog-${name}" = {
186 description = "VirtualBox Test Machine Log Socket For ${name}";
187 wantedBy = [ "sockets.target" ];
188 before = [ "multi-user.target" ];
189 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
190 socketConfig.Accept = true;
193 systemd.services."vboxtestlog-${name}@" = {
194 description = "VirtualBox Test Machine Log For ${name}";
195 serviceConfig.StandardInput = "socket";
196 serviceConfig.StandardOutput = "journal";
197 serviceConfig.SyslogIdentifier = "GUEST-${name}";
198 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
205 ${name}_sharepath = "${sharePath}"
208 def check_running_${name}():
209 cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
210 (status, _) = machine.execute(ru(cmd))
214 def cleanup_${name}():
215 if check_running_${name}():
216 machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
217 machine.succeed("rm -rf ${sharePath}")
218 machine.succeed("mkdir -p ${sharePath}")
219 machine.succeed("chown alice:users ${sharePath}")
222 def create_vm_${name}():
224 vbm("createvm --name ${name} ${createFlags}")
225 vbm("modifyvm ${name} ${vmFlags}")
226 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
227 vbm("storagectl ${name} ${controllerFlags}")
228 vbm("storageattach ${name} ${diskFlags}")
229 vbm("sharedfolder add ${name} ${sharedFlags}")
230 vbm("sharedfolder add ${name} ${nixstoreFlags}")
232 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
235 def destroy_vm_${name}():
237 vbm("unregistervm ${name} --delete")
240 def wait_for_vm_boot_${name}():
244 "while ! test -e ${sharePath}/boot-done; do "
245 "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
246 "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
252 def wait_for_ip_${name}(interface):
253 property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
254 getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
256 ip = machine.succeed(
258 "for i in $(seq 1000); do "
259 f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
260 'echo "$ipaddr"; exit 0; '
264 "echo 'Could not get IPv4 address for ${name}!' >&2; "
271 def wait_for_startup_${name}(nudge=lambda: None):
272 for _ in range(0, 130, 10):
274 if check_running_${name}():
277 raise Exception("VirtualBox VM didn't start up within 2 minutes")
280 def wait_for_shutdown_${name}():
281 for _ in range(0, 130, 10):
283 if not check_running_${name}():
285 raise Exception("VirtualBox VM didn't shut down within 2 minutes")
288 def shutdown_vm_${name}():
289 machine.succeed(ru("touch ${sharePath}/shutdown"))
292 "while test -e ${sharePath}/shutdown "
293 " -o -e ${sharePath}/boot-done; do "
294 "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
297 wait_for_shutdown_${name}()
305 "--hostonlyadapter2 vboxnet0"
308 # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
309 enableExtensionPackVMFlags = [
313 dhcpScript = pkgs: ''
314 ${pkgs.dhcpcd}/bin/dhcpcd eth0 eth1
316 otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
317 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
318 echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
321 sysdDetectVirt = pkgs: ''
322 ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
325 vboxVMs = mapAttrs createVM {
328 detectvirt.vmScript = sysdDetectVirt;
330 test1.vmFlags = hostonlyVMFlags;
331 test1.vmScript = dhcpScript;
333 test2.vmFlags = hostonlyVMFlags;
334 test2.vmScript = dhcpScript;
336 headless.virtualisation.virtualbox.headless = true;
337 headless.services.xserver.enable = false;
340 vboxVMsWithExtpack = mapAttrs createVM {
341 testExtensionPack.vmFlags = enableExtensionPackVMFlags;
344 mkVBoxTest = vboxHostConfig: vms: name: testScript: makeTest {
345 name = "virtualbox-${name}";
347 nodes.machine = { lib, config, ... }: {
349 mkVMConf = name: val: val.machine // { key = "${name}-config"; };
350 vmConfigs = mapAttrsToList mkVMConf vms;
351 in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
352 virtualisation.memorySize = 2048;
354 virtualisation.qemu.options = let
355 # IvyBridge is reasonably ancient to be compatible with recent
356 # Intel/AMD hosts and sufficient for the KVM flavor.
357 guestCpu = if config.virtualisation.virtualbox.host.enableKvm then "IvyBridge" else "kvm64";
358 in ["-cpu" "${guestCpu},svm=on,vmx=on"];
360 test-support.displayManager.auto.user = "alice";
361 users.users.alice.extraGroups = let
362 inherit (config.virtualisation.virtualbox.host) enableHardening;
363 in lib.mkIf enableHardening [ "vboxusers" ];
365 virtualisation.virtualbox.host = {
369 nixpkgs.config.allowUnfree = config.virtualisation.virtualbox.host.enableExtensionPack;
373 from shlex import quote
374 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
376 def ru(cmd: str) -> str:
377 return f"su - alice -c {quote(cmd)}"
380 def vbm(cmd: str) -> str:
381 return machine.succeed(ru(f"VBoxManage {cmd}"))
384 def remove_uuids(output: str) -> str:
386 [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
392 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
398 meta = with pkgs.lib.maintainers; {
399 maintainers = [ aszlig ];
403 unfreeTests = mapAttrs (mkVBoxTest { enableExtensionPack = true; } vboxVMsWithExtpack) {
404 enable-extension-pack = ''
405 create_vm_testExtensionPack()
406 vbm("startvm testExtensionPack")
407 wait_for_startup_testExtensionPack()
408 machine.screenshot("cli_started")
409 wait_for_vm_boot_testExtensionPack()
410 machine.screenshot("cli_booted")
412 with machine.nested("Checking for privilege escalation"):
413 machine.fail("test -e '/root/VirtualBox VMs'")
414 machine.fail("test -e '/root/.config/VirtualBox'")
415 machine.succeed("test -e '/home/alice/VirtualBox VMs'")
417 shutdown_vm_testExtensionPack()
418 destroy_vm_testExtensionPack()
422 kvmTests = mapAttrs (mkVBoxTest {
425 # Once the KVM version supports these, we can enable them.
426 addNetworkInterface = false;
427 enableHardening = false;
431 machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
432 wait_for_startup_headless()
433 wait_for_vm_boot_headless()
434 shutdown_vm_headless()
435 destroy_vm_headless()
439 in mapAttrs (mkVBoxTest {} vboxVMs) {
441 # Home to select Tools, down to move to the VM, enter to start it.
442 def send_vm_startup():
443 machine.send_key("home")
444 machine.send_key("down")
445 machine.send_key("ret")
449 machine.succeed(ru("VirtualBox >&2 &"))
450 machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'"))
452 machine.screenshot("gui_manager_started")
454 machine.screenshot("gui_manager_sent_startup")
455 wait_for_startup_simple(send_vm_startup)
456 machine.screenshot("gui_started")
457 wait_for_vm_boot_simple()
458 machine.screenshot("gui_booted")
461 machine.screenshot("gui_stopped")
462 machine.send_key("ctrl-q")
464 machine.screenshot("gui_manager_stopped")
470 vbm("startvm simple")
471 wait_for_startup_simple()
472 machine.screenshot("cli_started")
473 wait_for_vm_boot_simple()
474 machine.screenshot("cli_booted")
476 with machine.nested("Checking for privilege escalation"):
477 machine.fail("test -e '/root/VirtualBox VMs'")
478 machine.fail("test -e '/root/.config/VirtualBox'")
479 machine.succeed("test -e '/home/alice/VirtualBox VMs'")
487 machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
488 wait_for_startup_headless()
489 wait_for_vm_boot_headless()
490 shutdown_vm_headless()
491 destroy_vm_headless()
494 host-usb-permissions = ''
497 user_usb = remove_uuids(vbm("list usbhost"))
498 print(user_usb, file=sys.stderr)
499 root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
500 print(root_usb, file=sys.stderr)
502 if user_usb != root_usb:
503 raise Exception("USB host devices differ for root and normal user")
504 if "<none>" in user_usb:
505 raise Exception("No USB host devices found")
508 systemd-detect-virt = ''
509 create_vm_detectvirt()
510 vbm("startvm detectvirt")
511 wait_for_startup_detectvirt()
512 wait_for_vm_boot_detectvirt()
513 shutdown_vm_detectvirt()
514 result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
515 destroy_vm_detectvirt()
516 if result != "oracle":
517 raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
525 wait_for_startup_test1()
526 wait_for_vm_boot_test1()
529 wait_for_startup_test2()
530 wait_for_vm_boot_test2()
532 machine.screenshot("net_booted")
534 test1_ip = wait_for_ip_test1(1)
535 test2_ip = wait_for_ip_test2(1)
537 machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
538 machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")
540 machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
541 machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")
550 // (optionalAttrs enableKvm kvmTests)
551 // (optionalAttrs enableUnfree unfreeTests)