{ system ? builtins.currentSystem, config ? {}, pkgs ? import ../.. { inherit system config; }, debug ? false, enableUnfree ? false, # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests) # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs) # boot.extraModprobeConfig = "options kvm-intel nested=Y"; # Without this VirtualBox will use SW virtualization and will only be able # to run 32-bit guests. useKvmNestedVirt ? false, # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM. use64bitGuest ? false }: assert use64bitGuest -> useKvmNestedVirt; with import ../lib/testing-python.nix { inherit system pkgs; }; with pkgs.lib; let testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions; miniInit = '' #!${pkgs.runtimeShell} -xe export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.util-linux ]}" mkdir -p /run/dbus cat > /etc/passwd < /etc/group < /mnt/grub/grub.cfg <&2; " "exit 1" ) ).strip() return ip def wait_for_startup_${name}(nudge=lambda: None): for _ in range(0, 130, 10): machine.sleep(10) if check_running_${name}(): return nudge() raise Exception("VirtualBox VM didn't start up within 2 minutes") def wait_for_shutdown_${name}(): for _ in range(0, 130, 10): machine.sleep(10) if not check_running_${name}(): return raise Exception("VirtualBox VM didn't shut down within 2 minutes") def shutdown_vm_${name}(): machine.succeed(ru("touch ${sharePath}/shutdown")) machine.execute( "set -e; i=0; " "while test -e ${sharePath}/shutdown " " -o -e ${sharePath}/boot-done; do " "sleep 1; i=$(($i + 1)); [ $i -le 3600 ]; " "done" ) wait_for_shutdown_${name}() ''; }; hostonlyVMFlags = [ "--nictype1 virtio" "--nictype2 virtio" "--nic2 hostonly" "--hostonlyadapter2 vboxnet0" ]; # The VirtualBox Oracle Extension Pack lets you use USB 3.0 (xHCI). enableExtensionPackVMFlags = [ "--usbxhci on" ]; dhcpScript = pkgs: '' ${pkgs.dhcp}/bin/dhclient \ -lf /run/dhcp.leases \ -pf /run/dhclient.pid \ -v eth0 eth1 otherIP="$(${pkgs.netcat}/bin/nc -l 1234 || :)" ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP" echo "$otherIP reachable" | ${pkgs.netcat}/bin/nc -l 5678 || : ''; sysdDetectVirt = pkgs: '' ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result ''; vboxVMs = mapAttrs createVM { simple = {}; detectvirt.vmScript = sysdDetectVirt; test1.vmFlags = hostonlyVMFlags; test1.vmScript = dhcpScript; test2.vmFlags = hostonlyVMFlags; test2.vmScript = dhcpScript; headless.virtualisation.virtualbox.headless = true; headless.services.xserver.enable = false; }; vboxVMsWithExtpack = mapAttrs createVM { testExtensionPack.vmFlags = enableExtensionPackVMFlags; }; mkVBoxTest = useExtensionPack: vms: name: testScript: makeTest { name = "virtualbox-${name}"; machine = { lib, config, ... }: { imports = let mkVMConf = name: val: val.machine // { key = "${name}-config"; }; vmConfigs = mapAttrsToList mkVMConf vms; in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs; virtualisation.memorySize = 2048; virtualisation.qemu.options = if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else []; virtualisation.virtualbox.host.enable = true; test-support.displayManager.auto.user = "alice"; users.users.alice.extraGroups = let inherit (config.virtualisation.virtualbox.host) enableHardening; in lib.mkIf enableHardening (lib.singleton "vboxusers"); virtualisation.virtualbox.host.enableExtensionPack = useExtensionPack; nixpkgs.config.allowUnfree = useExtensionPack; }; testScript = '' from shlex import quote ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)} def ru(cmd: str) -> str: return f"su - alice -c {quote(cmd)}" def vbm(cmd: str) -> str: return machine.succeed(ru(f"VBoxManage {cmd}")) def remove_uuids(output: str) -> str: return "\n".join( [line for line in (output or "").splitlines() if not line.startswith("UUID:")] ) machine.wait_for_x() ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"} ${testScript} # (keep black happy) ''; meta = with pkgs.lib.maintainers; { maintainers = [ aszlig cdepillabout ]; }; }; unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) { enable-extension-pack = '' create_vm_testExtensionPack() vbm("startvm testExtensionPack") wait_for_startup_testExtensionPack() machine.screenshot("cli_started") wait_for_vm_boot_testExtensionPack() machine.screenshot("cli_booted") with machine.nested("Checking for privilege escalation"): machine.fail("test -e '/root/VirtualBox VMs'") machine.fail("test -e '/root/.config/VirtualBox'") machine.succeed("test -e '/home/alice/VirtualBox VMs'") shutdown_vm_testExtensionPack() destroy_vm_testExtensionPack() ''; }; in mapAttrs (mkVBoxTest false vboxVMs) { simple-gui = '' # Home to select Tools, down to move to the VM, enter to start it. def send_vm_startup(): machine.send_key("home") machine.send_key("down") machine.send_key("ret") create_vm_simple() machine.succeed(ru("VirtualBox &")) machine.wait_until_succeeds(ru("xprop -name 'Oracle VM VirtualBox Manager'")) machine.sleep(5) machine.screenshot("gui_manager_started") send_vm_startup() machine.screenshot("gui_manager_sent_startup") wait_for_startup_simple(send_vm_startup) machine.screenshot("gui_started") wait_for_vm_boot_simple() machine.screenshot("gui_booted") shutdown_vm_simple() machine.sleep(5) machine.screenshot("gui_stopped") machine.send_key("ctrl-q") machine.sleep(5) machine.screenshot("gui_manager_stopped") destroy_vm_simple() ''; simple-cli = '' create_vm_simple() vbm("startvm simple") wait_for_startup_simple() machine.screenshot("cli_started") wait_for_vm_boot_simple() machine.screenshot("cli_booted") with machine.nested("Checking for privilege escalation"): machine.fail("test -e '/root/VirtualBox VMs'") machine.fail("test -e '/root/.config/VirtualBox'") machine.succeed("test -e '/home/alice/VirtualBox VMs'") shutdown_vm_simple() destroy_vm_simple() ''; headless = '' create_vm_headless() machine.succeed(ru("VBoxHeadless --startvm headless & disown %1")) wait_for_startup_headless() wait_for_vm_boot_headless() shutdown_vm_headless() destroy_vm_headless() ''; host-usb-permissions = '' user_usb = remove_uuids(vbm("list usbhost")) print(user_usb, file=sys.stderr) root_usb = remove_uuids(machine.succeed("VBoxManage list usbhost")) print(root_usb, file=sys.stderr) if user_usb != root_usb: raise Exception("USB host devices differ for root and normal user") if "" in user_usb: raise Exception("No USB host devices found") ''; systemd-detect-virt = '' create_vm_detectvirt() vbm("startvm detectvirt") wait_for_startup_detectvirt() wait_for_vm_boot_detectvirt() shutdown_vm_detectvirt() result = machine.succeed(f"cat '{detectvirt_sharepath}/result'").strip() destroy_vm_detectvirt() if result != "oracle": raise Exception(f'systemd-detect-virt returned "{result}" instead of "oracle"') ''; net-hostonlyif = '' create_vm_test1() create_vm_test2() vbm("startvm test1") wait_for_startup_test1() wait_for_vm_boot_test1() vbm("startvm test2") wait_for_startup_test2() wait_for_vm_boot_test2() machine.screenshot("net_booted") test1_ip = wait_for_ip_test1(1) test2_ip = wait_for_ip_test2(1) machine.succeed(f"echo '{test2_ip}' | nc -N '{test1_ip}' 1234") machine.succeed(f"echo '{test1_ip}' | nc -N '{test2_ip}' 1234") machine.wait_until_succeeds(f"nc -N '{test1_ip}' 5678 < /dev/null >&2") machine.wait_until_succeeds(f"nc -N '{test2_ip}' 5678 < /dev/null >&2") shutdown_vm_test1() shutdown_vm_test2() destroy_vm_test1() destroy_vm_test2() ''; } // (if enableUnfree then unfreeTests else {})