Merge pull request #305845 from abathur/resholve_0.10.5
[NixPkgs.git] / nixos / tests / virtualbox.nix
blob3c2a391233dbdfe16cdcefeeb84b64d0bf85639b
1 { system ? builtins.currentSystem,
2   config ? {},
3   pkgs ? import ../.. { inherit system config; },
4   debug ? false,
5   enableUnfree ? false,
6   enableKvm ? false,
7   use64bitGuest ? true
8 }:
10 with import ../lib/testing-python.nix { inherit system pkgs; };
11 with pkgs.lib;
13 let
14   testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
15     guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
17     miniInit = ''
18       #!${pkgs.runtimeShell} -xe
19       export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}"
21       mkdir -p /run/dbus /var
22       ln -s /run /var
23       cat > /etc/passwd <<EOF
24       root:x:0:0::/root:/bin/false
25       messagebus:x:1:1::/run/dbus:/bin/false
26       EOF
27       cat > /etc/group <<EOF
28       root:x:0:
29       messagebus:x:1:
30       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}
38       i=0
39       while [ ! -e /mnt-root/shutdown ]; do
40         sleep 10
41         i=$(($i + 10))
42         [ $i -le 120 ] || fail
43       done
45       rm -f /mnt-root/boot-done /mnt-root/shutdown
46     '';
47   in {
48     boot.kernelParams = [
49       "console=tty0" "console=ttyS0" "ignore_loglevel"
50       "boot.trace" "panic=1" "boot.panic_on_fail"
51       "init=${pkgs.writeScript "mini-init.sh" miniInit}"
52     ];
54     fileSystems."/" = {
55       device = "vboxshare";
56       fsType = "vboxsf";
57     };
59     virtualisation.virtualbox.guest.enable = true;
61     boot.initrd.kernelModules = [
62       "af_packet" "vboxsf"
63       "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
64     ];
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}
70     '';
72     boot.initrd.postMountCommands = ''
73       touch /mnt-root/boot-done
74       hostname "${vmName}"
75       mkdir -p /nix/store
76       unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
77         mount -t vboxsf nixstore /nix/store
78         exec "$stage2Init"
79       '
80       poweroff -f
81     '';
83     system.requiredKernelConfig = with config.lib.kernelConfig; [
84       (isYes "SERIAL_8250_CONSOLE")
85       (isYes "SERIAL_8250")
86     ];
88     networking.usePredictableInterfaceNames = false;
89   };
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";
100       modules = [
101         ../modules/profiles/minimal.nix
102         (testVMConfig vmName vmScript)
103       ];
104     }).config;
105   in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
106     preVM = ''
107       mkdir -p "$out"
108       diskImage="$(pwd)/qimage"
109       ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
110     '';
112     postVM = ''
113       echo "creating VirtualBox disk image..."
114       ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
115         "$diskImage" "$out/disk.vdi"
116     '';
118     buildInputs = [ pkgs.util-linux pkgs.perl ];
119   } ''
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
124     mkdir /mnt
125     mount /dev/vda1 /mnt
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
132     set root=hd0,1
133     linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
134     initrd /initrd
135     boot
136     GRUB
137     umount /mnt
138   '');
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"}"
147       "--register"
148     ];
150     vmFlags = mkFlags ([
151       "--uart1 0x3F8 4"
152       "--uartmode1 client /run/virtualbox-log-${name}.sock"
153       "--memory 768"
154       "--audio none"
155     ] ++ (attrs.vmFlags or []));
157     controllerFlags = mkFlags [
158       "--name SATA"
159       "--add sata"
160       "--bootable on"
161       "--hostiocache on"
162     ];
164     diskFlags = mkFlags [
165       "--storagectl SATA"
166       "--port 0"
167       "--device 0"
168       "--type hdd"
169       "--mtype immutable"
170       "--medium ${testVM name attrs}/disk.vdi"
171     ];
173     sharedFlags = mkFlags [
174       "--name vboxshare"
175       "--hostpath ${sharePath}"
176     ];
178     nixstoreFlags = mkFlags [
179       "--name nixstore"
180       "--hostpath /nix/store"
181       "--readonly"
182     ];
183   in {
184     machine = {
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;
191       };
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";
199       };
200     };
202     testSubs = ''
205       ${name}_sharepath = "${sharePath}"
208       def check_running_${name}():
209           cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
210           (status, _) = machine.execute(ru(cmd))
211           return status == 0
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}():
223           cleanup_${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}():
236           cleanup_${name}()
237           vbm("unregistervm ${name} --delete")
240       def wait_for_vm_boot_${name}():
241           machine.execute(
242               ru(
243                   "set -e; i=0; "
244                   "while ! test -e ${sharePath}/boot-done; do "
245                   "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
246                   "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
247                   "done"
248               )
249           )
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(
257               ru(
258                   "for i in $(seq 1000); do "
259                   f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
260                   'echo "$ipaddr"; exit 0; '
261                   "fi; "
262                   "sleep 1; "
263                   "done; "
264                   "echo 'Could not get IPv4 address for ${name}!' >&2; "
265                   "exit 1"
266               )
267           ).strip()
268           return ip
271       def wait_for_startup_${name}(nudge=lambda: None):
272           for _ in range(0, 130, 10):
273               machine.sleep(10)
274               if check_running_${name}():
275                   return
276               nudge()
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):
282               machine.sleep(10)
283               if not check_running_${name}():
284                   return
285           raise Exception("VirtualBox VM didn't shut down within 2 minutes")
288       def shutdown_vm_${name}():
289           machine.succeed(ru("touch ${sharePath}/shutdown"))
290           machine.execute(
291               "set -e; i=0; "
292               "while test -e ${sharePath}/shutdown "
293               "        -o -e ${sharePath}/boot-done; do "
294               "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
295               "done"
296           )
297           wait_for_shutdown_${name}()
298     '';
299   };
301   hostonlyVMFlags = [
302     "--nictype1 virtio"
303     "--nictype2 virtio"
304     "--nic2 hostonly"
305     "--hostonlyadapter2 vboxnet0"
306   ];
308   # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
309   enableExtensionPackVMFlags = [
310     "--usbxhci on"
311   ];
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 || :
319   '';
321   sysdDetectVirt = pkgs: ''
322     ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
323   '';
325   vboxVMs = mapAttrs createVM {
326     simple = {};
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;
338   };
340   vboxVMsWithExtpack = mapAttrs createVM {
341     testExtensionPack.vmFlags = enableExtensionPackVMFlags;
342   };
344   mkVBoxTest = vboxHostConfig: vms: name: testScript: makeTest {
345     name = "virtualbox-${name}";
347     nodes.machine = { lib, config, ... }: {
348       imports = let
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 = {
366         enable = true;
367       } // vboxHostConfig;
369       nixpkgs.config.allowUnfree = config.virtualisation.virtualbox.host.enableExtensionPack;
370     };
372     testScript = ''
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:
385           return "\n".join(
386               [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
387           )
390       machine.wait_for_x()
392       ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
394       ${testScript}
395       # (keep black happy)
396     '';
398     meta = with pkgs.lib.maintainers; {
399       maintainers = [ aszlig ];
400     };
401   };
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()
419     '';
420   };
422   kvmTests = mapAttrs (mkVBoxTest {
423     enableKvm = true;
425     # Once the KVM version supports these, we can enable them.
426     addNetworkInterface = false;
427     enableHardening = false;
428   } vboxVMs) {
429     kvm-headless = ''
430       create_vm_headless()
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()
436     '';
437   };
439 in mapAttrs (mkVBoxTest {} vboxVMs) {
440   simple-gui = ''
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")
448     create_vm_simple()
449     machine.succeed(ru("VirtualBox >&2 &"))
450     machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'"))
451     machine.sleep(5)
452     machine.screenshot("gui_manager_started")
453     send_vm_startup()
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")
459     shutdown_vm_simple()
460     machine.sleep(5)
461     machine.screenshot("gui_stopped")
462     machine.send_key("ctrl-q")
463     machine.sleep(5)
464     machine.screenshot("gui_manager_stopped")
465     destroy_vm_simple()
466   '';
468   simple-cli = ''
469     create_vm_simple()
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'")
481     shutdown_vm_simple()
482     destroy_vm_simple()
483   '';
485   headless = ''
486     create_vm_headless()
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()
492   '';
494   host-usb-permissions = ''
495     import sys
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")
506   '';
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"')
518   '';
520   net-hostonlyif = ''
521     create_vm_test1()
522     create_vm_test2()
524     vbm("startvm test1")
525     wait_for_startup_test1()
526     wait_for_vm_boot_test1()
528     vbm("startvm test2")
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")
543     shutdown_vm_test1()
544     shutdown_vm_test2()
546     destroy_vm_test1()
547     destroy_vm_test2()
548   '';
550 // (optionalAttrs enableKvm kvmTests)
551 // (optionalAttrs enableUnfree unfreeTests)