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