about summary refs log tree commit diff
path: root/nixpkgs/nixos/tests/virtualbox.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/tests/virtualbox.nix')
-rw-r--r--nixpkgs/nixos/tests/virtualbox.nix538
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 {})