nixpkgs/nixos/tests/virtualbox.nix
Alyssa Ross 253fa03ea9
nixosTests.virtualbox.net-hostonlyif: use dhcpcd
dhclient is no longer built by default in the dhcp package, so this
test has been broken since that change was made.  To fix, switch to
dhcpcd.  dhcpcd insists on writing into /var/run, so we need to ensure
that exists.

Fixes: a2c379d4b6 ("dhcp: make client and relay component optional")
2022-06-26 18:12:13 +00:00

523 lines
15 KiB
Nix

{ system ? builtins.currentSystem,
config ? {},
pkgs ? import ../.. { inherit system config; },
debug ? false,
enableUnfree ? false,
use64bitGuest ? true
}:
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 /var
ln -s /run /var
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}"
];
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.util-linux}/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.runtimeShell} -c '
mount -t vboxsf nixstore /nix/store
exec "$stage2Init"
'
poweroff -f
'';
system.requiredKernelConfig = with config.lib.kernelConfig; [
(isYes "SERIAL_8250_CONSOLE")
(isYes "SERIAL_8250")
];
networking.usePredictableInterfaceNames = false;
};
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 if debug then "machine.execute(ru('${logcmd} & disown'))" else "pass";
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.util-linux 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 ([
"--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 = "journal";
serviceConfig.SyslogIdentifier = "GUEST-${name}";
serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
};
};
testSubs = ''
${name}_sharepath = "${sharePath}"
def check_running_${name}():
cmd = "VBoxManage list runningvms | grep -q '^\"${name}\"'"
(status, _) = machine.execute(ru(cmd))
return status == 0
def cleanup_${name}():
if check_running_${name}():
machine.execute(ru("VBoxManage controlvm ${name} poweroff"))
machine.succeed("rm -rf ${sharePath}")
machine.succeed("mkdir -p ${sharePath}")
machine.succeed("chown alice:users ${sharePath}")
def create_vm_${name}():
cleanup_${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}")
${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
def destroy_vm_${name}():
cleanup_${name}()
vbm("unregistervm ${name} --delete")
def wait_for_vm_boot_${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"
)
)
def wait_for_ip_${name}(interface):
property = f"/VirtualBox/GuestInfo/Net/{interface}/V4/IP"
getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
ip = machine.succeed(
ru(
"for i in $(seq 1000); do "
f'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"
)
).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.dhcpcd}/bin/dhcpcd 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}";
nodes.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 = ["-cpu" "kvm64,vmx=on"];
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 >&2 &"))
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 >&2 & disown %1"))
wait_for_startup_headless()
wait_for_vm_boot_headless()
shutdown_vm_headless()
destroy_vm_headless()
'';
host-usb-permissions = ''
import sys
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 "<none>" 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 {})