1{
2 system ? builtins.currentSystem,
3 config ? { },
4 pkgs ? import ../.. { inherit system config; },
5 debug ? false,
6 enableUnfree ? false,
7 enableKvm ? false,
8 use64bitGuest ? true,
9}:
10
11with import ../lib/testing-python.nix { inherit system pkgs; };
12with pkgs.lib;
13
14let
15 testVMConfig =
16 vmName: attrs:
17 {
18 config,
19 pkgs,
20 lib,
21 ...
22 }:
23 let
24 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
25
26 miniInit = ''
27 #!${pkgs.runtimeShell} -xe
28 export PATH="${
29 lib.makeBinPath [
30 pkgs.coreutils
31 pkgs.util-linux
32 ]
33 }"
34
35 mkdir -p /run/dbus /var
36 ln -s /run /var
37 cat > /etc/passwd <<EOF
38 root:x:0:0::/root:/bin/false
39 messagebus:x:1:1::/run/dbus:/bin/false
40 EOF
41 cat > /etc/group <<EOF
42 root:x:0:
43 messagebus:x:1:
44 EOF
45
46 "${pkgs.dbus}/bin/dbus-daemon" --fork \
47 --config-file="${pkgs.dbus}/share/dbus-1/system.conf"
48
49 ${guestAdditions}/bin/VBoxService
50 ${(attrs.vmScript or (const "")) pkgs}
51
52 i=0
53 while [ ! -e /mnt-root/shutdown ]; do
54 sleep 10
55 i=$(($i + 10))
56 [ $i -le 120 ] || fail
57 done
58
59 rm -f /mnt-root/boot-done /mnt-root/shutdown
60 '';
61 in
62 {
63 boot.kernelParams = [
64 "console=tty0"
65 "console=ttyS0"
66 "ignore_loglevel"
67 "boot.trace"
68 "panic=1"
69 "boot.panic_on_fail"
70 "init=${pkgs.writeScript "mini-init.sh" miniInit}"
71 ];
72
73 fileSystems."/" = {
74 device = "vboxshare";
75 fsType = "vboxsf";
76 };
77
78 virtualisation.virtualbox.guest.enable = true;
79
80 boot.initrd.kernelModules = [
81 "af_packet"
82 "vboxsf"
83 "virtio"
84 "virtio_pci"
85 "virtio_ring"
86 "virtio_net"
87 "vboxguest"
88 ];
89
90 boot.initrd.extraUtilsCommands = ''
91 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
92 copy_bin_and_libs "${pkgs.util-linux}/bin/unshare"
93 ${(attrs.extraUtilsCommands or (const "")) pkgs}
94 '';
95
96 boot.initrd.postMountCommands = ''
97 touch /mnt-root/boot-done
98 hostname "${vmName}"
99 mkdir -p /nix/store
100 unshare -m ${escapeShellArg pkgs.runtimeShell} -c '
101 mount -t vboxsf nixstore /nix/store
102 exec "$stage2Init"
103 '
104 poweroff -f
105 '';
106
107 system.requiredKernelConfig = with config.lib.kernelConfig; [
108 (isYes "SERIAL_8250_CONSOLE")
109 (isYes "SERIAL_8250")
110 ];
111
112 networking.usePredictableInterfaceNames = false;
113 };
114
115 mkLog =
116 logfile: tag:
117 let
118 rotated = map (i: "${logfile}.${toString i}") (range 1 9);
119 all = concatMapStringsSep " " (f: "\"${f}\"") ([ logfile ] ++ rotated);
120 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
121 in
122 if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";
123
124 testVM =
125 vmName: vmScript:
126 let
127 cfg =
128 (import ../lib/eval-config.nix {
129 system = if use64bitGuest then "x86_64-linux" else "i686-linux";
130 modules = [
131 (testVMConfig vmName vmScript)
132 ];
133 }).config;
134 in
135 pkgs.vmTools.runInLinuxVM (
136 pkgs.runCommand "virtualbox-image"
137 {
138 preVM = ''
139 mkdir -p "$out"
140 diskImage="$(pwd)/qimage"
141 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
142 '';
143
144 postVM = ''
145 echo "creating VirtualBox disk image..."
146 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
147 "$diskImage" "$out/disk.vdi"
148 '';
149
150 buildInputs = [
151 pkgs.util-linux
152 pkgs.perl
153 ];
154 }
155 ''
156 ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
157 ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
158 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
159 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
160 mkdir /mnt
161 mount /dev/vda1 /mnt
162 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
163 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
164
165 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
166
167 cat > /mnt/grub/grub.cfg <<GRUB
168 set root=hd0,1
169 linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
170 initrd /initrd
171 boot
172 GRUB
173 umount /mnt
174 ''
175 );
176
177 createVM =
178 name: attrs:
179 let
180 mkFlags = concatStringsSep " ";
181
182 sharePath = "/home/alice/vboxshare-${name}";
183
184 createFlags = mkFlags [
185 "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
186 "--register"
187 ];
188
189 vmFlags = mkFlags (
190 [
191 "--uart1 0x3F8 4"
192 "--uartmode1 client /run/virtualbox-log-${name}.sock"
193 "--memory 768"
194 "--audio none"
195 ]
196 ++ (attrs.vmFlags or [ ])
197 );
198
199 controllerFlags = mkFlags [
200 "--name SATA"
201 "--add sata"
202 "--bootable on"
203 "--hostiocache on"
204 ];
205
206 diskFlags = mkFlags [
207 "--storagectl SATA"
208 "--port 0"
209 "--device 0"
210 "--type hdd"
211 "--mtype immutable"
212 "--medium ${testVM name attrs}/disk.vdi"
213 ];
214
215 sharedFlags = mkFlags [
216 "--name vboxshare"
217 "--hostpath ${sharePath}"
218 ];
219
220 nixstoreFlags = mkFlags [
221 "--name nixstore"
222 "--hostpath /nix/store"
223 "--readonly"
224 ];
225 in
226 {
227 machine = {
228 systemd.sockets."vboxtestlog-${name}" = {
229 description = "VirtualBox Test Machine Log Socket For ${name}";
230 wantedBy = [ "sockets.target" ];
231 before = [ "multi-user.target" ];
232 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
233 socketConfig.Accept = true;
234 };
235
236 systemd.services."vboxtestlog-${name}@" = {
237 description = "VirtualBox Test Machine Log For ${name}";
238 serviceConfig.StandardInput = "socket";
239 serviceConfig.StandardOutput = "journal";
240 serviceConfig.SyslogIdentifier = "GUEST-${name}";
241 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
242 };
243 };
244
245 testSubs = ''
246
247
248 ${name}_sharepath = "${sharePath}"
249
250
251 def check_running_${name}():
252 cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
253 (status, _) = machine.execute(ru(cmd))
254 return status == 0
255
256
257 def cleanup_${name}():
258 if check_running_${name}():
259 machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
260 machine.succeed("rm -rf ${sharePath}")
261 machine.succeed("mkdir -p ${sharePath}")
262 machine.succeed("chown alice:users ${sharePath}")
263
264
265 def create_vm_${name}():
266 cleanup_${name}()
267 vbm("createvm --name ${name} ${createFlags}")
268 vbm("modifyvm ${name} ${vmFlags}")
269 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
270 vbm("storagectl ${name} ${controllerFlags}")
271 vbm("storageattach ${name} ${diskFlags}")
272 vbm("sharedfolder add ${name} ${sharedFlags}")
273 vbm("sharedfolder add ${name} ${nixstoreFlags}")
274
275 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
276
277
278 def destroy_vm_${name}():
279 cleanup_${name}()
280 vbm("unregistervm ${name} --delete")
281
282
283 def wait_for_vm_boot_${name}():
284 machine.execute(
285 ru(
286 "set -e; i=0; "
287 "while ! test -e ${sharePath}/boot-done; do "
288 "sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; "
289 "VBoxManage list runningvms | grep -q '^\"${name}\"'; "
290 "done"
291 )
292 )
293
294
295 def wait_for_ip_${name}(interface):
296 property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
297 getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
298
299 ip = machine.succeed(
300 ru(
301 "for i in $(seq 1000); do "
302 f'if ipaddr="$({getip})" && [ -n "$ipaddr" ]; then '
303 'echo "$ipaddr"; exit 0; '
304 "fi; "
305 "sleep 1; "
306 "done; "
307 "echo 'Could not get IPv4 address for ${name}!' >&2; "
308 "exit 1"
309 )
310 ).strip()
311 return ip
312
313
314 def wait_for_startup_${name}(nudge=lambda: None):
315 for _ in range(0, 130, 10):
316 machine.sleep(10)
317 if check_running_${name}():
318 return
319 nudge()
320 raise Exception("VirtualBox VM didn't start up within 2 minutes")
321
322
323 def wait_for_shutdown_${name}():
324 for _ in range(0, 130, 10):
325 machine.sleep(10)
326 if not check_running_${name}():
327 return
328 raise Exception("VirtualBox VM didn't shut down within 2 minutes")
329
330
331 def shutdown_vm_${name}():
332 machine.succeed(ru("touch ${sharePath}/shutdown"))
333 machine.execute(
334 "set -e; i=0; "
335 "while test -e ${sharePath}/shutdown "
336 " -o -e ${sharePath}/boot-done; do "
337 "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; "
338 "done"
339 )
340 wait_for_shutdown_${name}()
341 '';
342 };
343
344 hostonlyVMFlags = [
345 "--nictype1 virtio"
346 "--nictype2 virtio"
347 "--nic2 hostonly"
348 "--hostonlyadapter2 vboxnet0"
349 ];
350
351 # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
352 enableExtensionPackVMFlags = [
353 "--usbxhci on"
354 ];
355
356 dhcpScript = pkgs: ''
357 ${pkgs.dhcpcd}/bin/dhcpcd eth0 eth1
358
359 otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
360 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
361 echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
362 '';
363
364 sysdDetectVirt = pkgs: ''
365 ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
366 '';
367
368 vboxVMs = mapAttrs createVM {
369 simple = { };
370
371 detectvirt.vmScript = sysdDetectVirt;
372
373 test1.vmFlags = hostonlyVMFlags;
374 test1.vmScript = dhcpScript;
375
376 test2.vmFlags = hostonlyVMFlags;
377 test2.vmScript = dhcpScript;
378
379 headless.virtualisation.virtualbox.headless = true;
380 headless.services.xserver.enable = false;
381 };
382
383 vboxVMsWithExtpack = mapAttrs createVM {
384 testExtensionPack.vmFlags = enableExtensionPackVMFlags;
385 };
386
387 mkVBoxTest =
388 vboxHostConfig: vms: name: testScript:
389 makeTest {
390 name = "virtualbox-${name}";
391
392 nodes.machine =
393 { lib, config, ... }:
394 {
395 imports =
396 let
397 mkVMConf = name: val: val.machine // { key = "${name}-config"; };
398 vmConfigs = mapAttrsToList mkVMConf vms;
399 in
400 [
401 ./common/user-account.nix
402 ./common/x11.nix
403 ]
404 ++ vmConfigs;
405 virtualisation.memorySize = 2048;
406
407 virtualisation.qemu.options =
408 let
409 # IvyBridge is reasonably ancient to be compatible with recent
410 # Intel/AMD hosts and sufficient for the KVM flavor.
411 guestCpu = if config.virtualisation.virtualbox.host.enableKvm then "IvyBridge" else "kvm64";
412 in
413 [
414 "-cpu"
415 "${guestCpu},svm=on,vmx=on"
416 ];
417
418 test-support.displayManager.auto.user = "alice";
419 users.users.alice.extraGroups =
420 let
421 inherit (config.virtualisation.virtualbox.host) enableHardening;
422 in
423 lib.mkIf enableHardening [ "vboxusers" ];
424
425 virtualisation.virtualbox.host = {
426 enable = true;
427 }
428 // vboxHostConfig;
429
430 nixpkgs.config.allowUnfree = config.virtualisation.virtualbox.host.enableExtensionPack;
431 };
432
433 testScript = ''
434 from shlex import quote
435 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
436
437 def ru(cmd: str) -> str:
438 return f"su - alice -c {quote(cmd)}"
439
440
441 def vbm(cmd: str) -> str:
442 return machine.succeed(ru(f"VBoxManage {cmd}"))
443
444
445 def remove_uuids(output: str) -> str:
446 return "\n".join(
447 [line for line in (output or "").splitlines() if not line.startswith("UUID:")]
448 )
449
450
451 machine.wait_for_x()
452
453 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
454
455 ${testScript}
456 # (keep black happy)
457 '';
458
459 meta = with pkgs.lib.maintainers; {
460 maintainers = [ aszlig ];
461 };
462 };
463
464 unfreeTests = mapAttrs (mkVBoxTest { enableExtensionPack = true; } vboxVMsWithExtpack) {
465 enable-extension-pack = ''
466 create_vm_testExtensionPack()
467 vbm("startvm testExtensionPack")
468 wait_for_startup_testExtensionPack()
469 machine.screenshot("cli_started")
470 wait_for_vm_boot_testExtensionPack()
471 machine.screenshot("cli_booted")
472
473 with machine.nested("Checking for privilege escalation"):
474 machine.fail("test -e '/root/VirtualBox VMs'")
475 machine.fail("test -e '/root/.config/VirtualBox'")
476 machine.succeed("test -e '/home/alice/VirtualBox VMs'")
477
478 shutdown_vm_testExtensionPack()
479 destroy_vm_testExtensionPack()
480 '';
481 };
482
483 kvmTests =
484 mapAttrs
485 (mkVBoxTest {
486 enableKvm = true;
487
488 # Once the KVM version supports these, we can enable them.
489 addNetworkInterface = false;
490 enableHardening = false;
491 } vboxVMs)
492 {
493 kvm-headless = ''
494 create_vm_headless()
495 machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
496 wait_for_startup_headless()
497 wait_for_vm_boot_headless()
498 shutdown_vm_headless()
499 destroy_vm_headless()
500 '';
501 };
502
503in
504mapAttrs (mkVBoxTest { } vboxVMs) {
505 simple-gui = ''
506 # Home to select Tools, down to move to the VM, enter to start it.
507 def send_vm_startup():
508 machine.send_key("home")
509 machine.send_key("down")
510 machine.send_key("ret")
511
512
513 create_vm_simple()
514 machine.succeed(ru("VirtualBox >&2 &"))
515 machine.wait_until_succeeds(ru("xprop -name 'Oracle VirtualBox Manager'"))
516 machine.sleep(5)
517 machine.screenshot("gui_manager_started")
518 send_vm_startup()
519 machine.screenshot("gui_manager_sent_startup")
520 wait_for_startup_simple(send_vm_startup)
521 machine.screenshot("gui_started")
522 wait_for_vm_boot_simple()
523 machine.screenshot("gui_booted")
524 shutdown_vm_simple()
525 machine.sleep(5)
526 machine.screenshot("gui_stopped")
527 machine.send_key("ctrl-q")
528 machine.sleep(5)
529 machine.screenshot("gui_manager_stopped")
530 destroy_vm_simple()
531 '';
532
533 simple-cli = ''
534 create_vm_simple()
535 vbm("startvm simple")
536 wait_for_startup_simple()
537 machine.screenshot("cli_started")
538 wait_for_vm_boot_simple()
539 machine.screenshot("cli_booted")
540
541 with machine.nested("Checking for privilege escalation"):
542 machine.fail("test -e '/root/VirtualBox VMs'")
543 machine.fail("test -e '/root/.config/VirtualBox'")
544 machine.succeed("test -e '/home/alice/VirtualBox VMs'")
545
546 shutdown_vm_simple()
547 destroy_vm_simple()
548 '';
549
550 headless = ''
551 create_vm_headless()
552 machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1"))
553 wait_for_startup_headless()
554 wait_for_vm_boot_headless()
555 shutdown_vm_headless()
556 destroy_vm_headless()
557 '';
558
559 host-usb-permissions = ''
560 import sys
561
562 user_usb = remove_uuids(vbm("list usbhost"))
563 print(user_usb, file=sys.stderr)
564 root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost"))
565 print(root_usb, file=sys.stderr)
566
567 if user_usb != root_usb:
568 raise Exception("USB host devices differ for root and normal user")
569 if "<none>" in user_usb:
570 raise Exception("No USB host devices found")
571 '';
572
573 systemd-detect-virt = ''
574 create_vm_detectvirt()
575 vbm("startvm detectvirt")
576 wait_for_startup_detectvirt()
577 wait_for_vm_boot_detectvirt()
578 shutdown_vm_detectvirt()
579 result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip()
580 destroy_vm_detectvirt()
581 if result != "oracle":
582 raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"')
583 '';
584
585 net-hostonlyif = ''
586 create_vm_test1()
587 create_vm_test2()
588
589 vbm("startvm test1")
590 wait_for_startup_test1()
591 wait_for_vm_boot_test1()
592
593 vbm("startvm test2")
594 wait_for_startup_test2()
595 wait_for_vm_boot_test2()
596
597 machine.screenshot("net_booted")
598
599 test1_ip = wait_for_ip_test1(1)
600 test2_ip = wait_for_ip_test2(1)
601
602 machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234")
603 machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234")
604
605 machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2")
606 machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2")
607
608 shutdown_vm_test1()
609 shutdown_vm_test2()
610
611 destroy_vm_test1()
612 destroy_vm_test2()
613 '';
614}
615// (optionalAttrs enableKvm kvmTests)
616// (optionalAttrs enableUnfree unfreeTests)