python.pkgs.pyqt5: 5.14.2 -> 5.15.0
[NixPkgs.git] / nixos / tests / virtualbox.nix
blob0d9eafa4a20f303835ee029ea10509db54b38b58
1 { system ? builtins.currentSystem,
2   config ? {},
3   pkgs ? import ../.. { inherit system config; },
4   debug ? false,
5   enableUnfree ? false,
6   # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests)
7   # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs)
8   #   boot.extraModprobeConfig = "options kvm-intel nested=Y";
9   # Without this VirtualBox will use SW virtualization and will only be able
10   # to run 32-bit guests.
11   useKvmNestedVirt ? false,
12   # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM.
13   use64bitGuest ? false
16 assert use64bitGuest -> useKvmNestedVirt;
18 with import ../lib/testing-python.nix { inherit system pkgs; };
19 with pkgs.lib;
21 let
22   testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
23     guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
25     miniInit = ''
26       #!${pkgs.runtimeShell} -xe
27       export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.utillinux ]}"
29       mkdir -p /run/dbus
30       cat > /etc/passwd <<EOF
31       root:x:0:0::/root:/bin/false
32       messagebus:x:1:1::/run/dbus:/bin/false
33       EOF
34       cat > /etc/group <<EOF
35       root:x:0:
36       messagebus:x:1:
37       EOF
39       "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork \
40         --config-file="${pkgs.dbus.daemon}/share/dbus-1/system.conf"
42       ${guestAdditions}/bin/VBoxService
43       ${(attrs.vmScript or (const "")) pkgs}
45       i=0
46       while [ ! -e /mnt-root/shutdown ]; do
47         sleep 10
48         i=$(($i + 10))
49         [ $i -le 120 ] || fail
50       done
52       rm -f /mnt-root/boot-done /mnt-root/shutdown
53     '';
54   in {
55     boot.kernelParams = [
56       "console=tty0" "console=ttyS0" "ignore_loglevel"
57       "boot.trace" "panic=1" "boot.panic_on_fail"
58       "init=${pkgs.writeScript "mini-init.sh" miniInit}"
59     ];
61     fileSystems."/" = {
62       device = "vboxshare";
63       fsType = "vboxsf";
64     };
66     virtualisation.virtualbox.guest.enable = true;
68     boot.initrd.kernelModules = [
69       "af_packet" "vboxsf"
70       "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
71     ];
73     boot.initrd.extraUtilsCommands = ''
74       copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
75       copy_bin_and_libs "${pkgs.utillinux}/bin/unshare"
76       ${(attrs.extraUtilsCommands or (const "")) pkgs}
77     '';
79     boot.initrd.postMountCommands = ''
80       touch /mnt-root/boot-done
81       hostname "${vmName}"
82       mkdir -p /nix/store
83       unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
84         mount -t vboxsf nixstore /nix/store
85         exec "$stage2Init"
86       '
87       poweroff -f
88     '';
90     system.requiredKernelConfig = with config.lib.kernelConfig; [
91       (isYes "SERIAL_8250_CONSOLE")
92       (isYes "SERIAL_8250")
93     ];
95     networking.usePredictableInterfaceNames = false;
96   };
98   mkLog = logfile: tag: let
99     rotated = map (i: "${logfile}.${toString i}") (range 1 9);
100     all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
101     logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
102   in if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";
104   testVM = vmName: vmScript: let
105     cfg = (import ../lib/eval-config.nix {
106       system = if use64bitGuest then "x86_64-linux" else "i686-linux";
107       modules = [
108         ../modules/profiles/minimal.nix
109         (testVMConfig vmName vmScript)
110       ];
111     }).config;
112   in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
113     preVM = ''
114       mkdir -p "$out"
115       diskImage="$(pwd)/qimage"
116       ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
117     '';
119     postVM = ''
120       echo "creating VirtualBox disk image..."
121       ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
122         "$diskImage" "$out/disk.vdi"
123     '';
125     buildInputs = [ pkgs.utillinux pkgs.perl ];
126   } ''
127     ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
128     ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
129     ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
130     ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
131     mkdir /mnt
132     mount /dev/vda1 /mnt
133     cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
134     cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
136     ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
138     cat > /mnt/grub/grub.cfg <<GRUB
139     set root=hd0,1
140     linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
141     initrd /initrd
142     boot
143     GRUB
144     umount /mnt
145   '');
147   createVM = name: attrs: let
148     mkFlags = concatStringsSep " ";
150     sharePath = "/home/alice/vboxshare-${name}";
152     createFlags = mkFlags [
153       "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
154       "--register"
155     ];
157     vmFlags = mkFlags ([
158       "--uart1 0x3F8 4"
159       "--uartmode1 client /run/virtualbox-log-${name}.sock"
160       "--memory 768"
161       "--audio none"
162     ] ++ (attrs.vmFlags or []));
164     controllerFlags = mkFlags [
165       "--name SATA"
166       "--add sata"
167       "--bootable on"
168       "--hostiocache on"
169     ];
171     diskFlags = mkFlags [
172       "--storagectl SATA"
173       "--port 0"
174       "--device 0"
175       "--type hdd"
176       "--mtype immutable"
177       "--medium ${testVM name attrs}/disk.vdi"
178     ];
180     sharedFlags = mkFlags [
181       "--name vboxshare"
182       "--hostpath ${sharePath}"
183     ];
185     nixstoreFlags = mkFlags [
186       "--name nixstore"
187       "--hostpath /nix/store"
188       "--readonly"
189     ];
190   in {
191     machine = {
192       systemd.sockets."vboxtestlog-${name}" = {
193         description = "VirtualBox Test Machine Log Socket For ${name}";
194         wantedBy = [ "sockets.target" ];
195         before = [ "multi-user.target" ];
196         socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
197         socketConfig.Accept = true;
198       };
200       systemd.services."vboxtestlog-${name}@" = {
201         description = "VirtualBox Test Machine Log For ${name}";
202         serviceConfig.StandardInput = "socket";
203         serviceConfig.SyslogIdentifier = "GUEST-${name}";
204         serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
205       };
206     };
208     testSubs = ''
211       ${name}_sharepath = "${sharePath}"
214       def check_running_${name}():
215           cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
216           (status, _) = machine.execute(ru(cmd))
217           return status == 0
220       def cleanup_${name}():
221           if check_running_${name}():
222               machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
223           machine.succeed("rm -rf ${sharePath}")
224           machine.succeed("mkdir -p ${sharePath}")
225           machine.succeed("chown alice.users ${sharePath}")
228       def create_vm_${name}():
229           # fmt: off
230           vbm(f"createvm --name ${name} ${createFlags}")
231           vbm(f"modifyvm ${name} ${vmFlags}")
232           vbm(f"setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
233           vbm(f"storagectl ${name} ${controllerFlags}")
234           vbm(f"storageattach ${name} ${diskFlags}")
235           vbm(f"sharedfolder add ${name} ${sharedFlags}")
236           vbm(f"sharedfolder add ${name} ${nixstoreFlags}")
237           cleanup_${name}()
239           ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
240           # fmt: on
243       def destroy_vm_${name}():
244           cleanup_${name}()
245           vbm("unregistervm ${name} --delete")
248       def wait_for_vm_boot_${name}():
249           machine.execute(
250               ru(
251                   "set -e; i=0; "
252                   "while ! test -e ${sharePath}/boot-done; do "
253                   "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
254                   "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
255                   "done"
256               )
257           )
260       def wait_for_ip_${name}(interface):
261           property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
262           # fmt: off
263           getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
264           # fmt: on
266           ip = machine.succeed(
267               ru(
268                   "for i in $(seq 1000); do "
269                   f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
270                   'echo "$ipaddr"; exit 0; '
271                   "fi; "
272                   "sleep 1; "
273                   "done; "
274                   "echo 'Could not get IPv4 address for ${name}!' >&2; "
275                   "exit 1"
276               )
277           ).strip()
278           return ip
281       def wait_for_startup_${name}(nudge=lambda: None):
282           for _ in range(0, 130, 10):
283               machine.sleep(10)
284               if check_running_${name}():
285                   return
286               nudge()
287           raise Exception("VirtualBox VM didn't start up within 2 minutes")
290       def wait_for_shutdown_${name}():
291           for _ in range(0, 130, 10):
292               machine.sleep(10)
293               if not check_running_${name}():
294                   return
295           raise Exception("VirtualBox VM didn't shut down within 2 minutes")
298       def shutdown_vm_${name}():
299           machine.succeed(ru("touch ${sharePath}/shutdown"))
300           machine.execute(
301               "set -e; i=0; "
302               "while test -e ${sharePath}/shutdown "
303               "        -o -e ${sharePath}/boot-done; do "
304               "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
305               "done"
306           )
307           wait_for_shutdown_${name}()
308     '';
309   };
311   hostonlyVMFlags = [
312     "--nictype1 virtio"
313     "--nictype2 virtio"
314     "--nic2 hostonly"
315     "--hostonlyadapter2 vboxnet0"
316   ];
318   # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
319   enableExtensionPackVMFlags = [
320     "--usbxhci on"
321   ];
323   dhcpScript = pkgs: ''
324     ${pkgs.dhcp}/bin/dhclient \
325       -lf /run/dhcp.leases \
326       -pf /run/dhclient.pid \
327       -v eth0 eth1
329     otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
330     ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
331     echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
332   '';
334   sysdDetectVirt = pkgs: ''
335     ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
336   '';
338   vboxVMs = mapAttrs createVM {
339     simple = {};
341     detectvirt.vmScript = sysdDetectVirt;
343     test1.vmFlags = hostonlyVMFlags;
344     test1.vmScript = dhcpScript;
346     test2.vmFlags = hostonlyVMFlags;
347     test2.vmScript = dhcpScript;
349     headless.virtualisation.virtualbox.headless = true;
350     headless.services.xserver.enable = false;
351   };
353   vboxVMsWithExtpack = mapAttrs createVM {
354     testExtensionPack.vmFlags = enableExtensionPackVMFlags;
355   };
357   mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest {
358     name = "virtualbox-${name}";
360     machine = { lib, config, ... }: {
361       imports = let
362         mkVMConf = name: val: val.machine // { key = "${name}-config"; };
363         vmConfigs = mapAttrsToList mkVMConf vms;
364       in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
365       virtualisation.memorySize = 2048;
366       virtualisation.qemu.options =
367         if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else [];
368       virtualisation.virtualbox.host.enable = true;
369       test-support.displayManager.auto.user = "alice";
370       users.users.alice.extraGroups = let
371         inherit (config.virtualisation.virtualbox.host) enableHardening;
372       in lib.mkIf enableHardening (lib.singleton "vboxusers");
373       virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack;
374       nixpkgs.config.allowUnfree = useExtensionPack;
375     };
377     testScript = ''
378       from shlex import quote
379       ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
381       def ru(cmd: str) -> str:
382           return f"su - alice -c {quote(cmd)}"
385       def vbm(cmd: str) -> str:
386           return machine.succeed(ru(f"VBoxManage {cmd}"))
389       def remove_uuids(output: str) -> str:
390           return "\n".join(
391               [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
392           )
395       machine.wait_for_x()
397       # fmt: off
398       ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
399       # fmt: on
401       ${testScript}
402       # (keep black happy)
403     '';
405     meta = with pkgs.stdenv.lib.maintainers; {
406       maintainers = [ aszlig cdepillabout ];
407     };
408   };
410   unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) {
411     enable-extension-pack = ''
412       create_vm_testExtensionPack()
413       vbm("startvm testExtensionPack")
414       wait_for_startup_testExtensionPack()
415       machine.screenshot("cli_started")
416       wait_for_vm_boot_testExtensionPack()
417       machine.screenshot("cli_booted")
419       with machine.nested("Checking for privilege escalation"):
420           machine.fail("test -e '/root/VirtualBox VMs'")
421           machine.fail("test -e '/root/.config/VirtualBox'")
422           machine.succeed("test -e '/home/alice/VirtualBox VMs'")
424       shutdown_vm_testExtensionPack()
425       destroy_vm_testExtensionPack()
426     '';
427   };
429 in mapAttrs (mkVBoxTest false vboxVMs) {
430   simple-gui = ''
431     # Home to select Tools, down to move to the VM, enter to start it.
432     def send_vm_startup():
433         machine.send_key("home")
434         machine.send_key("down")
435         machine.send_key("ret")
438     create_vm_simple()
439     machine.succeed(ru("VirtualBox &"))
440     machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'"))
441     machine.sleep(5)
442     machine.screenshot("gui_manager_started")
443     send_vm_startup()
444     machine.screenshot("gui_manager_sent_startup")
445     wait_for_startup_simple(send_vm_startup)
446     machine.screenshot("gui_started")
447     wait_for_vm_boot_simple()
448     machine.screenshot("gui_booted")
449     shutdown_vm_simple()
450     machine.sleep(5)
451     machine.screenshot("gui_stopped")
452     machine.send_key("ctrl-q")
453     machine.sleep(5)
454     machine.screenshot("gui_manager_stopped")
455     destroy_vm_simple()
456   '';
458   simple-cli = ''
459     create_vm_simple()
460     vbm("startvm simple")
461     wait_for_startup_simple()
462     machine.screenshot("cli_started")
463     wait_for_vm_boot_simple()
464     machine.screenshot("cli_booted")
466     with machine.nested("Checking for privilege escalation"):
467         machine.fail("test -e '/root/VirtualBox VMs'")
468         machine.fail("test -e '/root/.config/VirtualBox'")
469         machine.succeed("test -e '/home/alice/VirtualBox VMs'")
471     shutdown_vm_simple()
472     destroy_vm_simple()
473   '';
475   headless = ''
476     create_vm_headless()
477     machine.succeed(ru("VBoxHeadless --startvm headless & disown %1"))
478     wait_for_startup_headless()
479     wait_for_vm_boot_headless()
480     shutdown_vm_headless()
481     destroy_vm_headless()
482   '';
484   host-usb-permissions = ''
485     user_usb = remove_uuids(vbm("list usbhost"))
486     print(user_usb, file=sys.stderr)
487     root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
488     print(root_usb, file=sys.stderr)
490     if user_usb != root_usb:
491         raise Exception("USB host devices differ for root and normal user")
492     if "<none>" in user_usb:
493         raise Exception("No USB host devices found")
494   '';
496   systemd-detect-virt = ''
497     create_vm_detectvirt()
498     vbm("startvm detectvirt")
499     wait_for_startup_detectvirt()
500     wait_for_vm_boot_detectvirt()
501     shutdown_vm_detectvirt()
502     result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
503     destroy_vm_detectvirt()
504     if result != "oracle":
505         raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
506   '';
508   net-hostonlyif = ''
509     create_vm_test1()
510     create_vm_test2()
512     vbm("startvm test1")
513     wait_for_startup_test1()
514     wait_for_vm_boot_test1()
516     vbm("startvm test2")
517     wait_for_startup_test2()
518     wait_for_vm_boot_test2()
520     machine.screenshot("net_booted")
522     test1_ip = wait_for_ip_test1(1)
523     test2_ip = wait_for_ip_test2(1)
525     machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
526     machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")
528     machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
529     machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")
531     shutdown_vm_test1()
532     shutdown_vm_test2()
534     destroy_vm_test1()
535     destroy_vm_test2()
536   '';
537 } // (if enableUnfree then unfreeTests else {})