at 23.11-pre 15 kB view raw
1{ system ? builtins.currentSystem, 2 config ? {}, 3 pkgs ? import ../.. { inherit system config; }, 4 debug ? false, 5 enableUnfree ? false, 6 use64bitGuest ? true 7}: 8 9with import ../lib/testing-python.nix { inherit system pkgs; }; 10with pkgs.lib; 11 12let 13 testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let 14 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions; 15 16 miniInit = '' 17 #!${pkgs.runtimeShell} -xe 18 export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}" 19 20 mkdir -p /run/dbus /var 21 ln -s /run /var 22 cat > /etc/passwd <<EOF 23 root:x:0:0::/root:/bin/false 24 messagebus:x:1:1::/run/dbus:/bin/false 25 EOF 26 cat > /etc/group <<EOF 27 root:x:0: 28 messagebus:x:1: 29 EOF 30 31 "${pkgs.dbus}/bin/dbus-daemon" --fork \ 32 --config-file="${pkgs.dbus}/share/dbus-1/system.conf" 33 34 ${guestAdditions}/bin/VBoxService 35 ${(attrs.vmScript or (const "")) pkgs} 36 37 i=0 38 while [ ! -e /mnt-root/shutdown ]; do 39 sleep 10 40 i=$(($i + 10)) 41 [ $i -le 120 ] || fail 42 done 43 44 rm -f /mnt-root/boot-done /mnt-root/shutdown 45 ''; 46 in { 47 boot.kernelParams = [ 48 "console=tty0" "console=ttyS0" "ignore_loglevel" 49 "boot.trace" "panic=1" "boot.panic_on_fail" 50 "init=${pkgs.writeScript "mini-init.sh" miniInit}" 51 ]; 52 53 fileSystems."/" = { 54 device = "vboxshare"; 55 fsType = "vboxsf"; 56 }; 57 58 virtualisation.virtualbox.guest.enable = true; 59 60 boot.initrd.kernelModules = [ 61 "af_packet" "vboxsf" 62 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest" 63 ]; 64 65 boot.initrd.extraUtilsCommands = '' 66 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf" 67 copy_bin_and_libs "${pkgs.util-linux}/bin/unshare" 68 ${(attrs.extraUtilsCommands or (const "")) pkgs} 69 ''; 70 71 boot.initrd.postMountCommands = '' 72 touch /mnt-root/boot-done 73 hostname "${vmName}" 74 mkdir -p /nix/store 75 unshare -m ${escapeShellArg pkgs.runtimeShell} -c ' 76 mount -t vboxsf nixstore /nix/store 77 exec "$stage2Init" 78 ' 79 poweroff -f 80 ''; 81 82 system.requiredKernelConfig = with config.lib.kernelConfig; [ 83 (isYes "SERIAL_8250_CONSOLE") 84 (isYes "SERIAL_8250") 85 ]; 86 87 networking.usePredictableInterfaceNames = false; 88 }; 89 90 mkLog = logfile: tag: let 91 rotated = map (i: "${logfile}.${toString i}") (range 1 9); 92 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated); 93 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\""; 94 in if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass"; 95 96 testVM = vmName: vmScript: let 97 cfg = (import ../lib/eval-config.nix { 98 system = if use64bitGuest then "x86_64-linux" else "i686-linux"; 99 modules = [ 100 ../modules/profiles/minimal.nix 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 = useExtensionPack: 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 virtualisation.qemu.options = ["-cpu" "kvm64,svm=on,vmx=on"]; 353 virtualisation.virtualbox.host.enable = true; 354 test-support.displayManager.auto.user = "alice"; 355 users.users.alice.extraGroups = let 356 inherit (config.virtualisation.virtualbox.host) enableHardening; 357 in lib.mkIf enableHardening (lib.singleton "vboxusers"); 358 virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack; 359 nixpkgs.config.allowUnfree = useExtensionPack; 360 }; 361 362 testScript = '' 363 from shlex import quote 364 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)} 365 366 def ru(cmd: str) -> str: 367 return f"su - alice -c {quote(cmd)}" 368 369 370 def vbm(cmd: str) -> str: 371 return machine.succeed(ru(f"VBoxManage {cmd}")) 372 373 374 def remove_uuids(output: str) -> str: 375 return "\n".join( 376 [line for line in (output or "").splitlines() if not line.startswith("UUID:")] 377 ) 378 379 380 machine.wait_for_x() 381 382 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"} 383 384 ${testScript} 385 # (keep black happy) 386 ''; 387 388 meta = with pkgs.lib.maintainers; { 389 maintainers = [ aszlig cdepillabout ]; 390 }; 391 }; 392 393 unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) { 394 enable-extension-pack = '' 395 create_vm_testExtensionPack() 396 vbm("startvm testExtensionPack") 397 wait_for_startup_testExtensionPack() 398 machine.screenshot("cli_started") 399 wait_for_vm_boot_testExtensionPack() 400 machine.screenshot("cli_booted") 401 402 with machine.nested("Checking for privilege escalation"): 403 machine.fail("test -e '/root/VirtualBox VMs'") 404 machine.fail("test -e '/root/.config/VirtualBox'") 405 machine.succeed("test -e '/home/alice/VirtualBox VMs'") 406 407 shutdown_vm_testExtensionPack() 408 destroy_vm_testExtensionPack() 409 ''; 410 }; 411 412in mapAttrs (mkVBoxTest false vboxVMs) { 413 simple-gui = '' 414 # Home to select Tools, down to move to the VM, enter to start it. 415 def send_vm_startup(): 416 machine.send_key("home") 417 machine.send_key("down") 418 machine.send_key("ret") 419 420 421 create_vm_simple() 422 machine.succeed(ru("VirtualBox >&2 &")) 423 machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'")) 424 machine.sleep(5) 425 machine.screenshot("gui_manager_started") 426 send_vm_startup() 427 machine.screenshot("gui_manager_sent_startup") 428 wait_for_startup_simple(send_vm_startup) 429 machine.screenshot("gui_started") 430 wait_for_vm_boot_simple() 431 machine.screenshot("gui_booted") 432 shutdown_vm_simple() 433 machine.sleep(5) 434 machine.screenshot("gui_stopped") 435 machine.send_key("ctrl-q") 436 machine.sleep(5) 437 machine.screenshot("gui_manager_stopped") 438 destroy_vm_simple() 439 ''; 440 441 simple-cli = '' 442 create_vm_simple() 443 vbm("startvm simple") 444 wait_for_startup_simple() 445 machine.screenshot("cli_started") 446 wait_for_vm_boot_simple() 447 machine.screenshot("cli_booted") 448 449 with machine.nested("Checking for privilege escalation"): 450 machine.fail("test -e '/root/VirtualBox VMs'") 451 machine.fail("test -e '/root/.config/VirtualBox'") 452 machine.succeed("test -e '/home/alice/VirtualBox VMs'") 453 454 shutdown_vm_simple() 455 destroy_vm_simple() 456 ''; 457 458 headless = '' 459 create_vm_headless() 460 machine.succeed(ru("VBoxHeadless --startvm headless >&2 & disown %1")) 461 wait_for_startup_headless() 462 wait_for_vm_boot_headless() 463 shutdown_vm_headless() 464 destroy_vm_headless() 465 ''; 466 467 host-usb-permissions = '' 468 import sys 469 470 user_usb = remove_uuids(vbm("list usbhost")) 471 print(user_usb, file=sys.stderr) 472 root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost")) 473 print(root_usb, file=sys.stderr) 474 475 if user_usb != root_usb: 476 raise Exception("USB host devices differ for root and normal user") 477 if "<none>" in user_usb: 478 raise Exception("No USB host devices found") 479 ''; 480 481 systemd-detect-virt = '' 482 create_vm_detectvirt() 483 vbm("startvm detectvirt") 484 wait_for_startup_detectvirt() 485 wait_for_vm_boot_detectvirt() 486 shutdown_vm_detectvirt() 487 result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip() 488 destroy_vm_detectvirt() 489 if result != "oracle": 490 raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"') 491 ''; 492 493 net-hostonlyif = '' 494 create_vm_test1() 495 create_vm_test2() 496 497 vbm("startvm test1") 498 wait_for_startup_test1() 499 wait_for_vm_boot_test1() 500 501 vbm("startvm test2") 502 wait_for_startup_test2() 503 wait_for_vm_boot_test2() 504 505 machine.screenshot("net_booted") 506 507 test1_ip = wait_for_ip_test1(1) 508 test2_ip = wait_for_ip_test2(1) 509 510 machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234") 511 machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234") 512 513 machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2") 514 machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2") 515 516 shutdown_vm_test1() 517 shutdown_vm_test2() 518 519 destroy_vm_test1() 520 destroy_vm_test2() 521 ''; 522} // (if enableUnfree then unfreeTests else {})