1import ./make-test.nix ({ pkgs, ... }: with pkgs.lib; let
2
3 debug = false;
4
5 testVMConfig = vmName: attrs: { config, pkgs, ... }: {
6 boot.kernelParams = let
7 miniInit = ''
8 #!${pkgs.stdenv.shell} -xe
9 export PATH="${pkgs.coreutils}/bin:${pkgs.utillinux}/bin"
10
11 ${pkgs.linuxPackages.virtualboxGuestAdditions}/bin/VBoxService
12 ${(attrs.vmScript or (const "")) pkgs}
13
14 i=0
15 while [ ! -e /mnt-root/shutdown ]; do
16 sleep 10
17 i=$(($i + 10))
18 [ $i -le 120 ] || fail
19 done
20
21 rm -f /mnt-root/boot-done /mnt-root/shutdown
22 '';
23 in [
24 "console=tty0" "console=ttyS0" "ignore_loglevel"
25 "boot.trace" "panic=1" "boot.panic_on_fail"
26 "init=${pkgs.writeScript "mini-init.sh" miniInit}"
27 ];
28
29 fileSystems."/" = {
30 device = "vboxshare";
31 fsType = "vboxsf";
32 };
33
34 virtualisation.virtualbox.guest.enable = true;
35
36 boot.initrd.kernelModules = [
37 "af_packet" "vboxsf"
38 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest"
39 ];
40
41 boot.initrd.extraUtilsCommands = ''
42 copy_bin_and_libs "${pkgs.linuxPackages.virtualboxGuestAdditions}/bin/mount.vboxsf"
43 copy_bin_and_libs "${pkgs.utillinux}/bin/unshare"
44 ${(attrs.extraUtilsCommands or (const "")) pkgs}
45 '';
46
47 boot.initrd.postMountCommands = ''
48 touch /mnt-root/boot-done
49 hostname "${vmName}"
50 mkdir -p /nix/store
51 unshare -m "@shell@" -c '
52 mount -t vboxsf nixstore /nix/store
53 exec "$stage2Init"
54 '
55 poweroff -f
56 '';
57
58 system.requiredKernelConfig = with config.lib.kernelConfig; [
59 (isYes "SERIAL_8250_CONSOLE")
60 (isYes "SERIAL_8250")
61 ];
62 };
63
64 mkLog = logfile: tag: let
65 rotated = map (i: "${logfile}.${toString i}") (range 1 9);
66 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated);
67 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\"";
68 in optionalString debug "$machine->execute(ru '${logcmd} & disown');";
69
70 testVM = vmName: vmScript: let
71 cfg = (import ../lib/eval-config.nix {
72 system = "i686-linux";
73 modules = [
74 ../modules/profiles/minimal.nix
75 (testVMConfig vmName vmScript)
76 ];
77 }).config;
78 in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
79 preVM = ''
80 mkdir -p "$out"
81 diskImage="$(pwd)/qimage"
82 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
83 '';
84
85 postVM = ''
86 echo "creating VirtualBox disk image..."
87 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
88 "$diskImage" "$out/disk.vdi"
89 '';
90
91 buildInputs = [ pkgs.utillinux pkgs.perl ];
92 } ''
93 ${pkgs.parted}/sbin/parted /dev/vda mklabel msdos
94 ${pkgs.parted}/sbin/parted /dev/vda -- mkpart primary ext2 1M -1s
95 . /sys/class/block/vda1/uevent
96 mknod /dev/vda1 b $MAJOR $MINOR
97
98 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
99 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
100 mkdir /mnt
101 mount /dev/vda1 /mnt
102 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
103 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
104
105 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
106
107 cat > /mnt/grub/grub.cfg <<GRUB
108 set root=hd0,1
109 linux /linux ${concatStringsSep " " cfg.boot.kernelParams}
110 initrd /initrd
111 boot
112 GRUB
113 umount /mnt
114 '');
115
116 createVM = name: attrs: let
117 mkFlags = concatStringsSep " ";
118
119 sharePath = "/home/alice/vboxshare-${name}";
120
121 createFlags = mkFlags [
122 "--ostype Linux26"
123 "--register"
124 ];
125
126 vmFlags = mkFlags ([
127 "--uart1 0x3F8 4"
128 "--uartmode1 client /run/virtualbox-log-${name}.sock"
129 ] ++ (attrs.vmFlags or []));
130
131 controllerFlags = mkFlags [
132 "--name SATA"
133 "--add sata"
134 "--bootable on"
135 "--hostiocache on"
136 ];
137
138 diskFlags = mkFlags [
139 "--storagectl SATA"
140 "--port 0"
141 "--device 0"
142 "--type hdd"
143 "--mtype immutable"
144 "--medium ${testVM name attrs}/disk.vdi"
145 ];
146
147 sharedFlags = mkFlags [
148 "--name vboxshare"
149 "--hostpath ${sharePath}"
150 ];
151
152 nixstoreFlags = mkFlags [
153 "--name nixstore"
154 "--hostpath /nix/store"
155 "--readonly"
156 ];
157 in {
158 machine = {
159 systemd.sockets = listToAttrs (singleton {
160 name = "vboxtestlog-${name}";
161 value = {
162 description = "VirtualBox Test Machine Log Socket";
163 wantedBy = [ "sockets.target" ];
164 before = [ "multi-user.target" ];
165 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock";
166 socketConfig.Accept = true;
167 };
168 });
169
170 systemd.services = listToAttrs (singleton {
171 name = "vboxtestlog-${name}@";
172 value = {
173 description = "VirtualBox Test Machine Log";
174 serviceConfig.StandardInput = "socket";
175 serviceConfig.StandardOutput = "syslog";
176 serviceConfig.SyslogIdentifier = "GUEST-${name}";
177 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
178 };
179 });
180 };
181
182 testSubs = ''
183 sub checkRunning_${name} {
184 my $cmd = 'VBoxManage list runningvms | grep -q "^\"${name}\""';
185 my ($status, $out) = $machine->execute(ru $cmd);
186 return $status == 0;
187 }
188
189 sub cleanup_${name} {
190 $machine->execute(ru "VBoxManage controlvm ${name} poweroff")
191 if checkRunning_${name};
192 $machine->succeed("rm -rf ${sharePath}");
193 $machine->succeed("mkdir -p ${sharePath}");
194 $machine->succeed("chown alice.users ${sharePath}");
195 }
196
197 sub createVM_${name} {
198 vbm("createvm --name ${name} ${createFlags}");
199 vbm("modifyvm ${name} ${vmFlags}");
200 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1");
201 vbm("storagectl ${name} ${controllerFlags}");
202 vbm("storageattach ${name} ${diskFlags}");
203 vbm("sharedfolder add ${name} ${sharedFlags}");
204 vbm("sharedfolder add ${name} ${nixstoreFlags}");
205 cleanup_${name};
206
207 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
208 }
209
210 sub destroyVM_${name} {
211 cleanup_${name};
212 vbm("unregistervm ${name} --delete");
213 }
214
215 sub waitForVMBoot_${name} {
216 $machine->execute(ru(
217 'set -e; i=0; '.
218 'while ! test -e ${sharePath}/boot-done; do '.
219 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '.
220 'VBoxManage list runningvms | grep -q "^\"${name}\""; '.
221 'done'
222 ));
223 }
224
225 sub waitForIP_${name} ($) {
226 my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP";
227 my $getip = "VBoxManage guestproperty get ${name} $property | ".
228 "sed -n -e 's/^Value: //p'";
229 my $ip = $machine->succeed(ru(
230 'for i in $(seq 1000); do '.
231 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '.
232 'echo "$ipaddr"; exit 0; '.
233 'fi; '.
234 'sleep 1; '.
235 'done; '.
236 'echo "Could not get IPv4 address for ${name}!" >&2; '.
237 'exit 1'
238 ));
239 chomp $ip;
240 return $ip;
241 }
242
243 sub waitForStartup_${name} {
244 for (my $i = 0; $i <= 120; $i += 10) {
245 $machine->sleep(10);
246 return if checkRunning_${name};
247 eval { $_[0]->() } if defined $_[0];
248 }
249 die "VirtualBox VM didn't start up within 2 minutes";
250 }
251
252 sub waitForShutdown_${name} {
253 for (my $i = 0; $i <= 120; $i += 10) {
254 $machine->sleep(10);
255 return unless checkRunning_${name};
256 }
257 die "VirtualBox VM didn't shut down within 2 minutes";
258 }
259
260 sub shutdownVM_${name} {
261 $machine->succeed(ru "touch ${sharePath}/shutdown");
262 $machine->waitUntilSucceeds(
263 "test ! -e ${sharePath}/shutdown ".
264 " -a ! -e ${sharePath}/boot-done"
265 );
266 waitForShutdown_${name};
267 }
268 '';
269 };
270
271 hostonlyVMFlags = [
272 "--nictype1 virtio"
273 "--nictype2 virtio"
274 "--nic2 hostonly"
275 "--hostonlyadapter2 vboxnet0"
276 ];
277
278 dhcpScript = pkgs: ''
279 ${pkgs.dhcp}/bin/dhclient \
280 -lf /run/dhcp.leases \
281 -pf /run/dhclient.pid \
282 -v eth0 eth1
283
284 otherIP="$(${pkgs.netcat}/bin/netcat -clp 1234 || :)"
285 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP"
286 echo "$otherIP reachable" | ${pkgs.netcat}/bin/netcat -clp 5678 || :
287 '';
288
289 vboxVMs = mapAttrs createVM {
290 simple = {};
291
292 test1.vmFlags = hostonlyVMFlags;
293 test1.vmScript = dhcpScript;
294
295 test2.vmFlags = hostonlyVMFlags;
296 test2.vmScript = dhcpScript;
297 };
298
299in {
300 name = "virtualbox";
301 meta = with pkgs.stdenv.lib.maintainers; {
302 maintainers = [ aszlig wkennington ];
303 };
304
305 machine = { pkgs, lib, config, ... }: {
306 imports = let
307 mkVMConf = name: val: val.machine // { key = "${name}-config"; };
308 vmConfigs = mapAttrsToList mkVMConf vboxVMs;
309 in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
310 virtualisation.memorySize = 768;
311 virtualisation.virtualbox.host.enable = true;
312 users.extraUsers.alice.extraGroups = let
313 inherit (config.virtualisation.virtualbox.host) enableHardening;
314 in lib.mkIf enableHardening (lib.singleton "vboxusers");
315 };
316
317 testScript = ''
318 sub ru ($) {
319 my $esc = $_[0] =~ s/'/'\\${"'"}'/gr;
320 return "su - alice -c '$esc'";
321 }
322
323 sub vbm {
324 $machine->succeed(ru("VBoxManage ".$_[0]));
325 };
326
327 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vboxVMs)}
328
329 $machine->waitForX;
330
331 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
332
333 createVM_simple;
334
335 subtest "simple-gui", sub {
336 $machine->succeed(ru "VirtualBox &");
337 $machine->waitForWindow(qr/Oracle VM VirtualBox Manager/);
338 $machine->sleep(5);
339 $machine->screenshot("gui_manager_started");
340 $machine->sendKeys("ret");
341 $machine->screenshot("gui_manager_sent_startup");
342 waitForStartup_simple (sub {
343 $machine->sendKeys("ret");
344 });
345 $machine->screenshot("gui_started");
346 waitForVMBoot_simple;
347 $machine->screenshot("gui_booted");
348 shutdownVM_simple;
349 $machine->sleep(5);
350 $machine->screenshot("gui_stopped");
351 $machine->sendKeys("ctrl-q");
352 $machine->sleep(5);
353 $machine->screenshot("gui_manager_stopped");
354 };
355
356 cleanup_simple;
357
358 subtest "simple-cli", sub {
359 vbm("startvm simple");
360 waitForStartup_simple;
361 $machine->screenshot("cli_started");
362 waitForVMBoot_simple;
363 $machine->screenshot("cli_booted");
364 shutdownVM_simple;
365 };
366
367 subtest "privilege-escalation", sub {
368 $machine->fail("test -e '/root/VirtualBox VMs'");
369 $machine->fail("test -e '/root/.config/VirtualBox'");
370 $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
371 };
372
373 destroyVM_simple;
374
375 subtest "net-hostonlyif", sub {
376 createVM_test1;
377 createVM_test2;
378
379 vbm("startvm test1");
380 waitForStartup_test1;
381
382 vbm("startvm test2");
383 waitForStartup_test2;
384
385 waitForVMBoot_test1;
386 waitForVMBoot_test2;
387
388 $machine->screenshot("net_booted");
389
390 my $test1IP = waitForIP_test1 1;
391 my $test2IP = waitForIP_test2 1;
392
393 $machine->succeed("echo '$test2IP' | netcat -c '$test1IP' 1234");
394 $machine->succeed("echo '$test1IP' | netcat -c '$test2IP' 1234");
395
396 $machine->waitUntilSucceeds("netcat -c '$test1IP' 5678 >&2");
397 $machine->waitUntilSucceeds("netcat -c '$test2IP' 5678 >&2");
398
399 shutdownVM_test1;
400 shutdownVM_test2;
401
402 destroyVM_test1;
403 destroyVM_test2;
404 };
405 '';
406})