at master 18 kB view raw
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)