{ system ? builtins.currentSystem, debug ? false, enableUnfree ? false }: with import ../lib/testing.nix { inherit system; }; with pkgs.lib; let testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions; miniInit = '' #!${pkgs.stdenv.shell} -xe export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.utillinux ]}" mkdir -p /run/dbus cat > /etc/passwd <<EOF root:x:0:0::/root:/bin/false messagebus:x:1:1::/run/dbus:/bin/false EOF cat > /etc/group <<EOF root:x:0: messagebus:x:1: EOF "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork \ --config-file="${pkgs.dbus.daemon}/share/dbus-1/system.conf" ${guestAdditions}/bin/VBoxService ${(attrs.vmScript or (const "")) pkgs} i=0 while [ ! -e /mnt-root/shutdown ]; do sleep 10 i=$(($i + 10)) [ $i -le 120 ] || fail done rm -f /mnt-root/boot-done /mnt-root/shutdown ''; in { boot.kernelParams = [ "console=tty0" "console=ttyS0" "ignore_loglevel" "boot.trace" "panic=1" "boot.panic_on_fail" "init=${pkgs.writeScript "mini-init.sh" miniInit}" ]; # XXX: Remove this once TSS location detection has been fixed in VirtualBox boot.kernelPackages = pkgs.linuxPackages_4_9; fileSystems."/" = { device = "vboxshare"; fsType = "vboxsf"; }; virtualisation.virtualbox.guest.enable = true; boot.initrd.kernelModules = [ "af_packet" "vboxsf" "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest" ]; boot.initrd.extraUtilsCommands = '' copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf" copy_bin_and_libs "${pkgs.utillinux}/bin/unshare" ${(attrs.extraUtilsCommands or (const "")) pkgs} ''; boot.initrd.postMountCommands = '' touch /mnt-root/boot-done hostname "${vmName}" mkdir -p /nix/store unshare -m ${escapeShellArg pkgs.stdenv.shell} -c ' mount -t vboxsf nixstore /nix/store exec "$stage2Init" ' poweroff -f ''; system.requiredKernelConfig = with config.lib.kernelConfig; [ (isYes "SERIAL_8250_CONSOLE") (isYes "SERIAL_8250") ]; }; mkLog = logfile: tag: let rotated = map (i: "${logfile}.${toString i}") (range 1 9); all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated); logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\""; in optionalString debug "$machine->execute(ru '${logcmd} & disown');"; testVM = vmName: vmScript: let cfg = (import ../lib/eval-config.nix { system = "i686-linux"; modules = [ ../modules/profiles/minimal.nix (testVMConfig vmName vmScript) ]; }).config; in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" { preVM = '' mkdir -p "$out" diskImage="$(pwd)/qimage" ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M ''; postVM = '' echo "creating VirtualBox disk image..." ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \ "$diskImage" "$out/disk.vdi" ''; buildInputs = [ pkgs.utillinux pkgs.perl ]; } '' ${pkgs.parted}/sbin/parted --script /dev/vda mklabel msdos ${pkgs.parted}/sbin/parted --script /dev/vda -- mkpart primary ext2 1M -1s ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1 mkdir /mnt mount /dev/vda1 /mnt cp "${cfg.system.build.kernel}/bzImage" /mnt/linux cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda cat > /mnt/grub/grub.cfg <<GRUB set root=hd0,1 linux /linux ${concatStringsSep " " cfg.boot.kernelParams} initrd /initrd boot GRUB umount /mnt ''); createVM = name: attrs: let mkFlags = concatStringsSep " "; sharePath = "/home/alice/vboxshare-${name}"; createFlags = mkFlags [ "--ostype Linux26" "--register" ]; vmFlags = mkFlags ([ "--uart1 0x3F8 4" "--uartmode1 client /run/virtualbox-log-${name}.sock" "--memory 768" "--audio none" ] ++ (attrs.vmFlags or [])); controllerFlags = mkFlags [ "--name SATA" "--add sata" "--bootable on" "--hostiocache on" ]; diskFlags = mkFlags [ "--storagectl SATA" "--port 0" "--device 0" "--type hdd" "--mtype immutable" "--medium ${testVM name attrs}/disk.vdi" ]; sharedFlags = mkFlags [ "--name vboxshare" "--hostpath ${sharePath}" ]; nixstoreFlags = mkFlags [ "--name nixstore" "--hostpath /nix/store" "--readonly" ]; in { machine = { systemd.sockets."vboxtestlog-${name}" = { description = "VirtualBox Test Machine Log Socket For ${name}"; wantedBy = [ "sockets.target" ]; before = [ "multi-user.target" ]; socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock"; socketConfig.Accept = true; }; systemd.services."vboxtestlog-${name}@" = { description = "VirtualBox Test Machine Log For ${name}"; serviceConfig.StandardInput = "socket"; serviceConfig.StandardOutput = "syslog"; serviceConfig.SyslogIdentifier = "GUEST-${name}"; serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat"; }; }; testSubs = '' my ${"$" + name}_sharepath = '${sharePath}'; sub checkRunning_${name} { my $cmd = 'VBoxManage list runningvms | grep -q "^\"${name}\""'; my ($status, $out) = $machine->execute(ru $cmd); return $status == 0; } sub cleanup_${name} { $machine->execute(ru "VBoxManage controlvm ${name} poweroff") if checkRunning_${name}; $machine->succeed("rm -rf ${sharePath}"); $machine->succeed("mkdir -p ${sharePath}"); $machine->succeed("chown alice.users ${sharePath}"); } sub createVM_${name} { vbm("createvm --name ${name} ${createFlags}"); vbm("modifyvm ${name} ${vmFlags}"); vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1"); vbm("storagectl ${name} ${controllerFlags}"); vbm("storageattach ${name} ${diskFlags}"); vbm("sharedfolder add ${name} ${sharedFlags}"); vbm("sharedfolder add ${name} ${nixstoreFlags}"); cleanup_${name}; ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"} } sub destroyVM_${name} { cleanup_${name}; vbm("unregistervm ${name} --delete"); } sub waitForVMBoot_${name} { $machine->execute(ru( 'set -e; i=0; '. 'while ! test -e ${sharePath}/boot-done; do '. 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '. 'VBoxManage list runningvms | grep -q "^\"${name}\""; '. 'done' )); } sub waitForIP_${name} ($) { my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP"; my $getip = "VBoxManage guestproperty get ${name} $property | ". "sed -n -e 's/^Value: //p'"; my $ip = $machine->succeed(ru( 'for i in $(seq 1000); do '. 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '. 'echo "$ipaddr"; exit 0; '. 'fi; '. 'sleep 1; '. 'done; '. 'echo "Could not get IPv4 address for ${name}!" >&2; '. 'exit 1' )); chomp $ip; return $ip; } sub waitForStartup_${name} { for (my $i = 0; $i <= 120; $i += 10) { $machine->sleep(10); return if checkRunning_${name}; eval { $_[0]->() } if defined $_[0]; } die "VirtualBox VM didn't start up within 2 minutes"; } sub waitForShutdown_${name} { for (my $i = 0; $i <= 120; $i += 10) { $machine->sleep(10); return unless checkRunning_${name}; } die "VirtualBox VM didn't shut down within 2 minutes"; } sub shutdownVM_${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' ); waitForShutdown_${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.virtualbox.host.enable = true; services.xserver.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 = '' sub ru ($) { my $esc = $_[0] =~ s/'/'\\${"'"}'/gr; return "su - alice -c '$esc'"; } sub vbm { $machine->succeed(ru("VBoxManage ".$_[0])); }; sub removeUUIDs { return join("\n", grep { $_ !~ /^UUID:/ } split(/\n/, $_[0]))."\n"; } ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vms)} $machine->waitForX; ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"} ${testScript} ''; meta = with pkgs.stdenv.lib.maintainers; { maintainers = [ aszlig wkennington cdepillabout ]; }; }; unfreeTests = mapAttrs (mkVBoxTest true vboxVMsWithExtpack) { enable-extension-pack = '' createVM_testExtensionPack; vbm("startvm testExtensionPack"); waitForStartup_testExtensionPack; $machine->screenshot("cli_started"); waitForVMBoot_testExtensionPack; $machine->screenshot("cli_booted"); $machine->nest("Checking for privilege escalation", sub { $machine->fail("test -e '/root/VirtualBox VMs'"); $machine->fail("test -e '/root/.config/VirtualBox'"); $machine->succeed("test -e '/home/alice/VirtualBox VMs'"); }); shutdownVM_testExtensionPack; destroyVM_testExtensionPack; ''; }; in mapAttrs (mkVBoxTest false vboxVMs) { simple-gui = '' createVM_simple; $machine->succeed(ru "VirtualBox &"); $machine->waitUntilSucceeds( ru "xprop -name 'Oracle VM VirtualBox Manager'" ); $machine->sleep(5); $machine->screenshot("gui_manager_started"); $machine->sendKeys("ret"); $machine->screenshot("gui_manager_sent_startup"); waitForStartup_simple (sub { $machine->sendKeys("ret"); }); $machine->screenshot("gui_started"); waitForVMBoot_simple; $machine->screenshot("gui_booted"); shutdownVM_simple; $machine->sleep(5); $machine->screenshot("gui_stopped"); $machine->sendKeys("ctrl-q"); $machine->sleep(5); $machine->screenshot("gui_manager_stopped"); destroyVM_simple; ''; simple-cli = '' createVM_simple; vbm("startvm simple"); waitForStartup_simple; $machine->screenshot("cli_started"); waitForVMBoot_simple; $machine->screenshot("cli_booted"); $machine->nest("Checking for privilege escalation", sub { $machine->fail("test -e '/root/VirtualBox VMs'"); $machine->fail("test -e '/root/.config/VirtualBox'"); $machine->succeed("test -e '/home/alice/VirtualBox VMs'"); }); shutdownVM_simple; destroyVM_simple; ''; headless = '' createVM_headless; $machine->succeed(ru("VBoxHeadless --startvm headless & disown %1")); waitForStartup_headless; waitForVMBoot_headless; shutdownVM_headless; destroyVM_headless; ''; host-usb-permissions = '' my $userUSB = removeUUIDs vbm("list usbhost"); print STDERR $userUSB; my $rootUSB = removeUUIDs $machine->succeed("VBoxManage list usbhost"); print STDERR $rootUSB; die "USB host devices differ for root and normal user" if $userUSB ne $rootUSB; die "No USB host devices found" if $userUSB =~ /<none>/; ''; systemd-detect-virt = '' createVM_detectvirt; vbm("startvm detectvirt"); waitForStartup_detectvirt; waitForVMBoot_detectvirt; shutdownVM_detectvirt; my $result = $machine->succeed("cat '$detectvirt_sharepath/result'"); chomp $result; destroyVM_detectvirt; die "systemd-detect-virt returned \"$result\" instead of \"oracle\"" if $result ne "oracle"; ''; net-hostonlyif = '' createVM_test1; createVM_test2; vbm("startvm test1"); waitForStartup_test1; waitForVMBoot_test1; vbm("startvm test2"); waitForStartup_test2; waitForVMBoot_test2; $machine->screenshot("net_booted"); my $test1IP = waitForIP_test1 1; my $test2IP = waitForIP_test2 1; $machine->succeed("echo '$test2IP' | nc -N '$test1IP' 1234"); $machine->succeed("echo '$test1IP' | nc -N '$test2IP' 1234"); $machine->waitUntilSucceeds("nc -N '$test1IP' 5678 < /dev/null >&2"); $machine->waitUntilSucceeds("nc -N '$test2IP' 5678 < /dev/null >&2"); shutdownVM_test1; shutdownVM_test2; destroyVM_test1; destroyVM_test2; ''; } // (if enableUnfree then unfreeTests else {})