vuls: init at 0.27.0
[NixPkgs.git] / nixos / tests / virtualbox.nix
blob5fce3ba5481234fbc9cbfbb2fad11eed86f2420c
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         (testVMConfig vmName vmScript)
102       ];
103     }).config;
104   in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
105     preVM = ''
106       mkdir -p "$out"
107       diskImage="$(pwd)/qimage"
108       ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
109     '';
111     postVM = ''
112       echo "creating VirtualBox disk image..."
113       ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
114         "$diskImage" "$out/disk.vdi"
115     '';
117     buildInputs = [ pkgs.util-linux pkgs.perl ];
118   } ''
119     ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
120     ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
121     ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
122     ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
123     mkdir /mnt
124     mount /dev/vda1 /mnt
125     cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
126     cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
128     ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
130     cat > /mnt/grub/grub.cfg <<GRUB
131     set root=hd0,1
132     linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
133     initrd /initrd
134     boot
135     GRUB
136     umount /mnt
137   '');
139   createVM = name: attrs: let
140     mkFlags = concatStringsSep " ";
142     sharePath = "/home/alice/vboxshare-${name}";
144     createFlags = mkFlags [
145       "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
146       "--register"
147     ];
149     vmFlags = mkFlags ([
150       "--uart1 0x3F8 4"
151       "--uartmode1 client /run/virtualbox-log-${name}.sock"
152       "--memory 768"
153       "--audio none"
154     ] ++ (attrs.vmFlags or []));
156     controllerFlags = mkFlags [
157       "--name SATA"
158       "--add sata"
159       "--bootable on"
160       "--hostiocache on"
161     ];
163     diskFlags = mkFlags [
164       "--storagectl SATA"
165       "--port 0"
166       "--device 0"
167       "--type hdd"
168       "--mtype immutable"
169       "--medium ${testVM name attrs}/disk.vdi"
170     ];
172     sharedFlags = mkFlags [
173       "--name vboxshare"
174       "--hostpath ${sharePath}"
175     ];
177     nixstoreFlags = mkFlags [
178       "--name nixstore"
179       "--hostpath /nix/store"
180       "--readonly"
181     ];
182   in {
183     machine = {
184       systemd.sockets."vboxtestlog-${name}" = {
185         description = "VirtualBox Test Machine Log Socket For ${name}";
186         wantedBy = [ "sockets.target" ];
187         before = [ "multi-user.target" ];
188         socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
189         socketConfig.Accept = true;
190       };
192       systemd.services."vboxtestlog-${name}@" = {
193         description = "VirtualBox Test Machine Log For ${name}";
194         serviceConfig.StandardInput = "socket";
195         serviceConfig.StandardOutput = "journal";
196         serviceConfig.SyslogIdentifier = "GUEST-${name}";
197         serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
198       };
199     };
201     testSubs = ''
204       ${name}_sharepath = "${sharePath}"
207       def check_running_${name}():
208           cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
209           (status, _) = machine.execute(ru(cmd))
210           return status == 0
213       def cleanup_${name}():
214           if check_running_${name}():
215               machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
216           machine.succeed("rm -rf ${sharePath}")
217           machine.succeed("mkdir -p ${sharePath}")
218           machine.succeed("chown alice:users ${sharePath}")
221       def create_vm_${name}():
222           cleanup_${name}()
223           vbm("createvm --name ${name} ${createFlags}")
224           vbm("modifyvm ${name} ${vmFlags}")
225           vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
226           vbm("storagectl ${name} ${controllerFlags}")
227           vbm("storageattach ${name} ${diskFlags}")
228           vbm("sharedfolder add ${name} ${sharedFlags}")
229           vbm("sharedfolder add ${name} ${nixstoreFlags}")
231           ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
234       def destroy_vm_${name}():
235           cleanup_${name}()
236           vbm("unregistervm ${name} --delete")
239       def wait_for_vm_boot_${name}():
240           machine.execute(
241               ru(
242                   "set -e; i=0; "
243                   "while ! test -e ${sharePath}/boot-done; do "
244                   "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
245                   "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
246                   "done"
247               )
248           )
251       def wait_for_ip_${name}(interface):
252           property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
253           getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
255           ip = machine.succeed(
256               ru(
257                   "for i in $(seq 1000); do "
258                   f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
259                   'echo "$ipaddr"; exit 0; '
260                   "fi; "
261                   "sleep 1; "
262                   "done; "
263                   "echo 'Could not get IPv4 address for ${name}!' >&2; "
264                   "exit 1"
265               )
266           ).strip()
267           return ip
270       def wait_for_startup_${name}(nudge=lambda: None):
271           for _ in range(0, 130, 10):
272               machine.sleep(10)
273               if check_running_${name}():
274                   return
275               nudge()
276           raise Exception("VirtualBox VM didn't start up within 2 minutes")
279       def wait_for_shutdown_${name}():
280           for _ in range(0, 130, 10):
281               machine.sleep(10)
282               if not check_running_${name}():
283                   return
284           raise Exception("VirtualBox VM didn't shut down within 2 minutes")
287       def shutdown_vm_${name}():
288           machine.succeed(ru("touch ${sharePath}/shutdown"))
289           machine.execute(
290               "set -e; i=0; "
291               "while test -e ${sharePath}/shutdown "
292               "        -o -e ${sharePath}/boot-done; do "
293               "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
294               "done"
295           )
296           wait_for_shutdown_${name}()
297     '';
298   };
300   hostonlyVMFlags = [
301     "--nictype1 virtio"
302     "--nictype2 virtio"
303     "--nic2 hostonly"
304     "--hostonlyadapter2 vboxnet0"
305   ];
307   # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
308   enableExtensionPackVMFlags = [
309     "--usbxhci on"
310   ];
312   dhcpScript = pkgs: ''
313     ${pkgs.dhcpcd}/bin/dhcpcd eth0 eth1
315     otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
316     ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
317     echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
318   '';
320   sysdDetectVirt = pkgs: ''
321     ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
322   '';
324   vboxVMs = mapAttrs createVM {
325     simple = {};
327     detectvirt.vmScript = sysdDetectVirt;
329     test1.vmFlags = hostonlyVMFlags;
330     test1.vmScript = dhcpScript;
332     test2.vmFlags = hostonlyVMFlags;
333     test2.vmScript = dhcpScript;
335     headless.virtualisation.virtualbox.headless = true;
336     headless.services.xserver.enable = false;
337   };
339   vboxVMsWithExtpack = mapAttrs createVM {
340     testExtensionPack.vmFlags = enableExtensionPackVMFlags;
341   };
343   mkVBoxTest = vboxHostConfig: vms: name: testScript: makeTest {
344     name = "virtualbox-${name}";
346     nodes.machine = { lib, config, ... }: {
347       imports = let
348         mkVMConf = name: val: val.machine // { key = "${name}-config"; };
349         vmConfigs = mapAttrsToList mkVMConf vms;
350       in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
351       virtualisation.memorySize = 2048;
353       virtualisation.qemu.options = let
354         # IvyBridge is reasonably ancient to be compatible with recent
355         # Intel/AMD hosts and sufficient for the KVM flavor.
356         guestCpu = if config.virtualisation.virtualbox.host.enableKvm then "IvyBridge" else "kvm64";
357       in ["-cpu" "${guestCpu},svm=on,vmx=on"];
359       test-support.displayManager.auto.user = "alice";
360       users.users.alice.extraGroups = let
361         inherit (config.virtualisation.virtualbox.host) enableHardening;
362       in lib.mkIf enableHardening [ "vboxusers" ];
364       virtualisation.virtualbox.host = {
365         enable = true;
366       } // vboxHostConfig;
368       nixpkgs.config.allowUnfree = config.virtualisation.virtualbox.host.enableExtensionPack;
369     };
371     testScript = ''
372       from shlex import quote
373       ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
375       def ru(cmd: str) -> str:
376           return f"su - alice -c {quote(cmd)}"
379       def vbm(cmd: str) -> str:
380           return machine.succeed(ru(f"VBoxManage {cmd}"))
383       def remove_uuids(output: str) -> str:
384           return "\n".join(
385               [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
386           )
389       machine.wait_for_x()
391       ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
393       ${testScript}
394       # (keep black happy)
395     '';
397     meta = with pkgs.lib.maintainers; {
398       maintainers = [ aszlig ];
399     };
400   };
402   unfreeTests = mapAttrs (mkVBoxTest { enableExtensionPack = true; } vboxVMsWithExtpack) {
403     enable-extension-pack = ''
404       create_vm_testExtensionPack()
405       vbm("startvm testExtensionPack")
406       wait_for_startup_testExtensionPack()
407       machine.screenshot("cli_started")
408       wait_for_vm_boot_testExtensionPack()
409       machine.screenshot("cli_booted")
411       with machine.nested("Checking for privilege escalation"):
412           machine.fail("test -e '/root/VirtualBox VMs'")
413           machine.fail("test -e '/root/.config/VirtualBox'")
414           machine.succeed("test -e '/home/alice/VirtualBox VMs'")
416       shutdown_vm_testExtensionPack()
417       destroy_vm_testExtensionPack()
418     '';
419   };
421   kvmTests = mapAttrs (mkVBoxTest {
422     enableKvm = true;
424     # Once the KVM version supports these, we can enable them.
425     addNetworkInterface = false;
426     enableHardening = false;
427   } vboxVMs) {
428     kvm-headless = ''
429       create_vm_headless()
430       machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
431       wait_for_startup_headless()
432       wait_for_vm_boot_headless()
433       shutdown_vm_headless()
434       destroy_vm_headless()
435     '';
436   };
438 in mapAttrs (mkVBoxTest {} vboxVMs) {
439   simple-gui = ''
440     # Home to select Tools, down to move to the VM, enter to start it.
441     def send_vm_startup():
442         machine.send_key("home")
443         machine.send_key("down")
444         machine.send_key("ret")
447     create_vm_simple()
448     machine.succeed(ru("VirtualBox >&2 &"))
449     machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'"))
450     machine.sleep(5)
451     machine.screenshot("gui_manager_started")
452     send_vm_startup()
453     machine.screenshot("gui_manager_sent_startup")
454     wait_for_startup_simple(send_vm_startup)
455     machine.screenshot("gui_started")
456     wait_for_vm_boot_simple()
457     machine.screenshot("gui_booted")
458     shutdown_vm_simple()
459     machine.sleep(5)
460     machine.screenshot("gui_stopped")
461     machine.send_key("ctrl-q")
462     machine.sleep(5)
463     machine.screenshot("gui_manager_stopped")
464     destroy_vm_simple()
465   '';
467   simple-cli = ''
468     create_vm_simple()
469     vbm("startvm simple")
470     wait_for_startup_simple()
471     machine.screenshot("cli_started")
472     wait_for_vm_boot_simple()
473     machine.screenshot("cli_booted")
475     with machine.nested("Checking for privilege escalation"):
476         machine.fail("test -e '/root/VirtualBox VMs'")
477         machine.fail("test -e '/root/.config/VirtualBox'")
478         machine.succeed("test -e '/home/alice/VirtualBox VMs'")
480     shutdown_vm_simple()
481     destroy_vm_simple()
482   '';
484   headless = ''
485     create_vm_headless()
486     machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
487     wait_for_startup_headless()
488     wait_for_vm_boot_headless()
489     shutdown_vm_headless()
490     destroy_vm_headless()
491   '';
493   host-usb-permissions = ''
494     import sys
496     user_usb = remove_uuids(vbm("list usbhost"))
497     print(user_usb, file=sys.stderr)
498     root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
499     print(root_usb, file=sys.stderr)
501     if user_usb != root_usb:
502         raise Exception("USB host devices differ for root and normal user")
503     if "<none>" in user_usb:
504         raise Exception("No USB host devices found")
505   '';
507   systemd-detect-virt = ''
508     create_vm_detectvirt()
509     vbm("startvm detectvirt")
510     wait_for_startup_detectvirt()
511     wait_for_vm_boot_detectvirt()
512     shutdown_vm_detectvirt()
513     result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
514     destroy_vm_detectvirt()
515     if result != "oracle":
516         raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
517   '';
519   net-hostonlyif = ''
520     create_vm_test1()
521     create_vm_test2()
523     vbm("startvm test1")
524     wait_for_startup_test1()
525     wait_for_vm_boot_test1()
527     vbm("startvm test2")
528     wait_for_startup_test2()
529     wait_for_vm_boot_test2()
531     machine.screenshot("net_booted")
533     test1_ip = wait_for_ip_test1(1)
534     test2_ip = wait_for_ip_test2(1)
536     machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
537     machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")
539     machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
540     machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")
542     shutdown_vm_test1()
543     shutdown_vm_test2()
545     destroy_vm_test1()
546     destroy_vm_test2()
547   '';
549 // (optionalAttrs enableKvm kvmTests)
550 // (optionalAttrs enableUnfree unfreeTests)