nixpkgs/nixos/tests/virtualbox.nix

513 lines
15 KiB
Nix
Raw Normal View History

{ system ? builtins.currentSystem,
config ? {},
pkgs ? import ../.. { inherit system config; },
debug ? false,
enableUnfree ? false
}:
with import ../lib/testing.nix { inherit system pkgs; };
with pkgs.lib;
let
2016-08-23 12:05:14 +01:00
testVMConfig = vmName: attrs: { config, pkgs, lib, ... }: let
guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions;
miniInit = ''
#!${pkgs.stdenv.shell} -xe
2016-08-23 12:05:14 +01:00
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"
2015-03-29 00:15:41 +00:00
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 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 {})