at 18.09-beta 15 kB view raw
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 {})