diff options
Diffstat (limited to 'nixpkgs/nixos/tests/virtualbox.nix')
-rw-r--r-- | nixpkgs/nixos/tests/virtualbox.nix | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/nixpkgs/nixos/tests/virtualbox.nix b/nixpkgs/nixos/tests/virtualbox.nix new file mode 100644 index 000000000000..844ce47d743f --- /dev/null +++ b/nixpkgs/nixos/tests/virtualbox.nix @@ -0,0 +1,538 @@ +{ 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, + # Whether to enable the virtual UART in VirtualBox guests, allowing to see + # the guest console. There is currently a bug in VirtualBox where this will + # cause a crash if running with SW virtualization + # (https://www.virtualbox.org/ticket/18632). If you need to debug the tests + # then enable this and nested KVM to work around the crash (see above). + enableVBoxUART ? false +}: + +assert use64bitGuest -> useKvmNestedVirt; +assert enableVBoxUART -> useKvmNestedVirt; # VirtualBox bug, see above + +with import ../lib/testing.nix { inherit system pkgs; }; +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 = if use64bitGuest then "x86_64-linux" else "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 ${if use64bitGuest then "Linux26_64" else "Linux26"}" + "--register" + ]; + + vmFlags = mkFlags ( + (optionals enableVBoxUART [ + "--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}" = mkIf enableVBoxUART { + 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}@" = mkIf enableVBoxUART { + 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.qemu.options = + if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else []; + 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 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"); + # Home to select Tools, down to move to the VM, enter to start it. + $machine->sendKeys("home"); + $machine->sendKeys("down"); + $machine->sendKeys("ret"); + $machine->screenshot("gui_manager_sent_startup"); + waitForStartup_simple (sub { + $machine->sendKeys("home"); + $machine->sendKeys("down"); + $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 {}) |