1{ system ? builtins.currentSystem, debug ? false, enableUnfree ? false }:
2
3with import ../lib/testing.nix { inherit system; };
4with pkgs.lib;
5
6let
7 testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
8 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
9
10 miniInit = ''
11 #!${pkgs.stdenv.shell} -xe
12 export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.utillinux ]}"
13
14 mkdir -p /run/dbus
15 cat > /etc/passwd <<EOF
16 root:x:0:0::/root:/bin/false
17 messagebus:x:1:1::/run/dbus:/bin/false
18 EOF
19 cat > /etc/group <<EOF
20 root:x:0:
21 messagebus:x:1:
22 EOF
23
24 "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork \
25 --config-file="${pkgs.dbus.daemon}/share/dbus-1/system.conf"
26
27 ${guestAdditions}/bin/VBoxService
28 ${(attrs.vmScript or (const "")) pkgs}
29
30 i=0
31 while [ ! -e /mnt-root/shutdown ]; do
32 sleep 10
33 i=$(($i + 10))
34 [ $i -le 120 ] || fail
35 done
36
37 rm -f /mnt-root/boot-done /mnt-root/shutdown
38 '';
39 in {
40 boot.kernelParams = [
41 "console=tty0" "console=ttyS0" "ignore_loglevel"
42 "boot.trace" "panic=1" "boot.panic_on_fail"
43 "init=${pkgs.writeScript "mini-init.sh" miniInit}"
44 ];
45
46 # XXX: Remove this once TSS location detection has been fixed in VirtualBox
47 boot.kernelPackages = pkgs.linuxPackages_4_9;
48
49 fileSystems."/" = {
50 device = "vboxshare";
51 fsType = "vboxsf";
52 };
53
54 virtualisation.virtualbox.guest.enable = true;
55
56 boot.initrd.kernelModules = [
57 "af_packet" "vboxsf"
58 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
59 ];
60
61 boot.initrd.extraUtilsCommands = ''
62 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf"
63 copy_bin_and_libs "${pkgs.utillinux}/bin/unshare"
64 ${(attrs.extraUtilsCommands or (const "")) pkgs}
65 '';
66
67 boot.initrd.postMountCommands = ''
68 touch /mnt-root/boot-done
69 hostname "${vmName}"
70 mkdir -p /nix/store
71 unshare -m ${escapeShellArg pkgs.stdenv.shell} -c '
72 mount -t vboxsf nixstore /nix/store
73 exec "$stage2Init"
74 '
75 poweroff -f
76 '';
77
78 system.requiredKernelConfig = with config.lib.kernelConfig; [
79 (isYes "SERIAL_8250_CONSOLE")
80 (isYes "SERIAL_8250")
81 ];
82 };
83
84 mkLog = logfile: tag: let
85 rotated = map (i: "${logfile}.${toString i}") (range 1 9);
86 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
87 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
88 in optionalString debug "$machine->execute(ru '${logcmd} & disown');";
89
90 testVM = vmName: vmScript: let
91 cfg = (import ../lib/eval-config.nix {
92 system = "i686-linux";
93 modules = [
94 ../modules/profiles/minimal.nix
95 (testVMConfig vmName vmScript)
96 ];
97 }).config;
98 in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
99 preVM = ''
100 mkdir -p "$out"
101 diskImage="$(pwd)/qimage"
102 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
103 '';
104
105 postVM = ''
106 echo "creating VirtualBox disk image..."
107 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
108 "$diskImage" "$out/disk.vdi"
109 '';
110
111 buildInputs = [ pkgs.utillinux pkgs.perl ];
112 } ''
113 ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos
114 ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s
115 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
116 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
117 mkdir /mnt
118 mount /dev/vda1 /mnt
119 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
120 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
121
122 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
123
124 cat > /mnt/grub/grub.cfg <<GRUB
125 set root=hd0,1
126 linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
127 initrd /initrd
128 boot
129 GRUB
130 umount /mnt
131 '');
132
133 createVM = name: attrs: let
134 mkFlags = concatStringsSep " ";
135
136 sharePath = "/home/alice/vboxshare-${name}";
137
138 createFlags = mkFlags [
139 "--ostype Linux26"
140 "--register"
141 ];
142
143 vmFlags = mkFlags ([
144 "--uart1 0x3F8 4"
145 "--uartmode1 client /run/virtualbox-log-${name}.sock"
146 "--memory 768"
147 "--audio none"
148 ] ++ (attrs.vmFlags or []));
149
150 controllerFlags = mkFlags [
151 "--name SATA"
152 "--add sata"
153 "--bootable on"
154 "--hostiocache on"
155 ];
156
157 diskFlags = mkFlags [
158 "--storagectl SATA"
159 "--port 0"
160 "--device 0"
161 "--type hdd"
162 "--mtype immutable"
163 "--medium ${testVM name attrs}/disk.vdi"
164 ];
165
166 sharedFlags = mkFlags [
167 "--name vboxshare"
168 "--hostpath ${sharePath}"
169 ];
170
171 nixstoreFlags = mkFlags [
172 "--name nixstore"
173 "--hostpath /nix/store"
174 "--readonly"
175 ];
176 in {
177 machine = {
178 systemd.sockets."vboxtestlog-${name}" = {
179 description = "VirtualBox Test Machine Log Socket For ${name}";
180 wantedBy = [ "sockets.target" ];
181 before = [ "multi-user.target" ];
182 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
183 socketConfig.Accept = true;
184 };
185
186 systemd.services."vboxtestlog-${name}@" = {
187 description = "VirtualBox Test Machine Log For ${name}";
188 serviceConfig.StandardInput = "socket";
189 serviceConfig.StandardOutput = "syslog";
190 serviceConfig.SyslogIdentifier = "GUEST-${name}";
191 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
192 };
193 };
194
195 testSubs = ''
196 my ${"$" + name}_sharepath = '${sharePath}';
197
198 sub checkRunning_${name} {
199 my $cmd = 'VBoxManage list runningvms | grep -q "^\"${name}\""';
200 my ($status, $out) = $machine->execute(ru $cmd);
201 return $status == 0;
202 }
203
204 sub cleanup_${name} {
205 $machine->execute(ru "VBoxManage controlvm ${name} poweroff")
206 if checkRunning_${name};
207 $machine->succeed("rm -rf ${sharePath}");
208 $machine->succeed("mkdir -p ${sharePath}");
209 $machine->succeed("chown alice.users ${sharePath}");
210 }
211
212 sub createVM_${name} {
213 vbm("createvm --name ${name} ${createFlags}");
214 vbm("modifyvm ${name} ${vmFlags}");
215 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1");
216 vbm("storagectl ${name} ${controllerFlags}");
217 vbm("storageattach ${name} ${diskFlags}");
218 vbm("sharedfolder add ${name} ${sharedFlags}");
219 vbm("sharedfolder add ${name} ${nixstoreFlags}");
220 cleanup_${name};
221
222 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
223 }
224
225 sub destroyVM_${name} {
226 cleanup_${name};
227 vbm("unregistervm ${name} --delete");
228 }
229
230 sub waitForVMBoot_${name} {
231 $machine->execute(ru(
232 'set -e; i=0; '.
233 'while ! test -e ${sharePath}/boot-done; do '.
234 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '.
235 'VBoxManage list runningvms | grep -q "^\"${name}\""; '.
236 'done'
237 ));
238 }
239
240 sub waitForIP_${name} ($) {
241 my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP";
242 my $getip = "VBoxManage guestproperty get ${name} $property | ".
243 "sed -n -e 's/^Value: //p'";
244 my $ip = $machine->succeed(ru(
245 'for i in $(seq 1000); do '.
246 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '.
247 'echo "$ipaddr"; exit 0; '.
248 'fi; '.
249 'sleep 1; '.
250 'done; '.
251 'echo "Could not get IPv4 address for ${name}!" >&2; '.
252 'exit 1'
253 ));
254 chomp $ip;
255 return $ip;
256 }
257
258 sub waitForStartup_${name} {
259 for (my $i = 0; $i <= 120; $i += 10) {
260 $machine->sleep(10);
261 return if checkRunning_${name};
262 eval { $_[0]->() } if defined $_[0];
263 }
264 die "VirtualBox VM didn't start up within 2 minutes";
265 }
266
267 sub waitForShutdown_${name} {
268 for (my $i = 0; $i <= 120; $i += 10) {
269 $machine->sleep(10);
270 return unless checkRunning_${name};
271 }
272 die "VirtualBox VM didn't shut down within 2 minutes";
273 }
274
275 sub shutdownVM_${name} {
276 $machine->succeed(ru "touch ${sharePath}/shutdown");
277 $machine->execute(
278 'set -e; i=0; '.
279 'while test -e ${sharePath}/shutdown '.
280 ' -o -e ${sharePath}/boot-done; do '.
281 'sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; '.
282 'done'
283 );
284 waitForShutdown_${name};
285 }
286 '';
287 };
288
289 hostonlyVMFlags = [
290 "--nictype1 virtio"
291 "--nictype2 virtio"
292 "--nic2 hostonly"
293 "--hostonlyadapter2 vboxnet0"
294 ];
295
296 # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI).
297 enableExtensionPackVMFlags = [
298 "--usbxhci on"
299 ];
300
301 dhcpScript = pkgs: ''
302 ${pkgs.dhcp}/bin/dhclient \
303 -lf /run/dhcp.leases \
304 -pf /run/dhclient.pid \
305 -v eth0 eth1
306
307 otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)"
308 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
309 echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || :
310 '';
311
312 sysdDetectVirt = pkgs: ''
313 ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result
314 '';
315
316 vboxVMs = mapAttrs createVM {
317 simple = {};
318
319 detectvirt.vmScript = sysdDetectVirt;
320
321 test1.vmFlags = hostonlyVMFlags;
322 test1.vmScript = dhcpScript;
323
324 test2.vmFlags = hostonlyVMFlags;
325 test2.vmScript = dhcpScript;
326
327 headless.virtualisation.virtualbox.headless = true;
328 headless.services.xserver.enable = false;
329 };
330
331 vboxVMsWithExtpack = mapAttrs createVM {
332 testExtensionPack.vmFlags = enableExtensionPackVMFlags;
333 };
334
335 mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest {
336 name = "virtualbox-${name}";
337
338 machine = { lib, config, ... }: {
339 imports = let
340 mkVMConf = name: val: val.machine // { key = "${name}-config"; };
341 vmConfigs = mapAttrsToList mkVMConf vms;
342 in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
343 virtualisation.memorySize = 2048;
344 virtualisation.virtualbox.host.enable = true;
345 services.xserver.displayManager.auto.user = "alice";
346 users.users.alice.extraGroups = let
347 inherit (config.virtualisation.virtualbox.host) enableHardening;
348 in lib.mkIf enableHardening (lib.singleton "vboxusers");
349 virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack;
350 nixpkgs.config.allowUnfree = useExtensionPack;
351 };
352
353 testScript = ''
354 sub ru ($) {
355 my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
356 return "su - alice -c '$esc'";
357 }
358
359 sub vbm {
360 $machine->succeed(ru("VBoxManage ".$_[0]));
361 };
362
363 sub removeUUIDs {
364 return join("\n", grep { $_ !~ /^UUID:/ } split(/\n/, $_[0]))."\n";
365 }
366
367 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)}
368
369 $machine->waitForX;
370
371 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
372
373 ${testScript}
374 '';
375
376 meta = with pkgs.stdenv.lib.maintainers; {
377 maintainers = [ aszlig wkennington cdepillabout ];
378 };
379 };
380
381 unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) {
382 enable-extension-pack = ''
383 createVM_testExtensionPack;
384 vbm("startvm testExtensionPack");
385 waitForStartup_testExtensionPack;
386 $machine->screenshot("cli_started");
387 waitForVMBoot_testExtensionPack;
388 $machine->screenshot("cli_booted");
389
390 $machine->nest("Checking for privilege escalation", sub {
391 $machine->fail("test -e '/root/VirtualBox VMs'");
392 $machine->fail("test -e '/root/.config/VirtualBox'");
393 $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
394 });
395
396 shutdownVM_testExtensionPack;
397 destroyVM_testExtensionPack;
398 '';
399 };
400
401in mapAttrs (mkVBoxTest false vboxVMs) {
402 simple-gui = ''
403 createVM_simple;
404 $machine->succeed(ru "VirtualBox &");
405 $machine->waitUntilSucceeds(
406 ru "xprop -name 'Oracle VM VirtualBox Manager'"
407 );
408 $machine->sleep(5);
409 $machine->screenshot("gui_manager_started");
410 $machine->sendKeys("ret");
411 $machine->screenshot("gui_manager_sent_startup");
412 waitForStartup_simple (sub {
413 $machine->sendKeys("ret");
414 });
415 $machine->screenshot("gui_started");
416 waitForVMBoot_simple;
417 $machine->screenshot("gui_booted");
418 shutdownVM_simple;
419 $machine->sleep(5);
420 $machine->screenshot("gui_stopped");
421 $machine->sendKeys("ctrl-q");
422 $machine->sleep(5);
423 $machine->screenshot("gui_manager_stopped");
424 destroyVM_simple;
425 '';
426
427 simple-cli = ''
428 createVM_simple;
429 vbm("startvm simple");
430 waitForStartup_simple;
431 $machine->screenshot("cli_started");
432 waitForVMBoot_simple;
433 $machine->screenshot("cli_booted");
434
435 $machine->nest("Checking for privilege escalation", sub {
436 $machine->fail("test -e '/root/VirtualBox VMs'");
437 $machine->fail("test -e '/root/.config/VirtualBox'");
438 $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
439 });
440
441 shutdownVM_simple;
442 destroyVM_simple;
443 '';
444
445 headless = ''
446 createVM_headless;
447 $machine->succeed(ru("VBoxHeadless --startvm headless & disown %1"));
448 waitForStartup_headless;
449 waitForVMBoot_headless;
450 shutdownVM_headless;
451 destroyVM_headless;
452 '';
453
454 host-usb-permissions = ''
455 my $userUSB = removeUUIDs vbm("list usbhost");
456 print STDERR $userUSB;
457 my $rootUSB = removeUUIDs $machine->succeed("VBoxManage list usbhost");
458 print STDERR $rootUSB;
459
460 die "USB host devices differ for root and normal user"
461 if $userUSB ne $rootUSB;
462 die "No USB host devices found" if $userUSB =~ /<none>/;
463 '';
464
465 systemd-detect-virt = ''
466 createVM_detectvirt;
467 vbm("startvm detectvirt");
468 waitForStartup_detectvirt;
469 waitForVMBoot_detectvirt;
470 shutdownVM_detectvirt;
471 my $result = $machine->succeed("cat '$detectvirt_sharepath/result'");
472 chomp $result;
473 destroyVM_detectvirt;
474 die "systemd-detect-virt returned \"$result\" instead of \"oracle\""
475 if $result ne "oracle";
476 '';
477
478 net-hostonlyif = ''
479 createVM_test1;
480 createVM_test2;
481
482 vbm("startvm test1");
483 waitForStartup_test1;
484 waitForVMBoot_test1;
485
486 vbm("startvm test2");
487 waitForStartup_test2;
488 waitForVMBoot_test2;
489
490 $machine->screenshot("net_booted");
491
492 my $test1IP = waitForIP_test1 1;
493 my $test2IP = waitForIP_test2 1;
494
495 $machine->succeed("echo '$test2IP' | nc -N '$test1IP' 1234");
496 $machine->succeed("echo '$test1IP' | nc -N '$test2IP' 1234");
497
498 $machine->waitUntilSucceeds("nc -N '$test1IP' 5678 < /dev/null >&2");
499 $machine->waitUntilSucceeds("nc -N '$test2IP' 5678 < /dev/null >&2");
500
501 shutdownVM_test1;
502 shutdownVM_test2;
503
504 destroyVM_test1;
505 destroyVM_test2;
506 '';
507} // (if enableUnfree then unfreeTests else {})