25bef2d8f9
The library does not depend on stdenv, that `stdenv` exposes `lib` is an artifact of the ancient origins of nixpkgs.
538 lines
16 KiB
Nix
538 lines
16 KiB
Nix
{ 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
|
|
}:
|
|
|
|
assert use64bitGuest -> useKvmNestedVirt;
|
|
|
|
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
|
|
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.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}():
|
|
# fmt: off
|
|
vbm(f"createvm --name ${name} ${createFlags}")
|
|
vbm(f"modifyvm ${name} ${vmFlags}")
|
|
vbm(f"setextradata ${name} VBoxInternal/PDM/HaltOnReset 1")
|
|
vbm(f"storagectl ${name} ${controllerFlags}")
|
|
vbm(f"storageattach ${name} ${diskFlags}")
|
|
vbm(f"sharedfolder add ${name} ${sharedFlags}")
|
|
vbm(f"sharedfolder add ${name} ${nixstoreFlags}")
|
|
cleanup_${name}()
|
|
|
|
${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"}
|
|
# fmt: on
|
|
|
|
|
|
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"
|
|
# fmt: off
|
|
getip = f"VBoxManage guestproperty get ${name} {property} | sed -n -e 's/^Value: //p'"
|
|
# fmt: on
|
|
|
|
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.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;
|
|
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()
|
|
|
|
# fmt: off
|
|
${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"}
|
|
# fmt: on
|
|
|
|
${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 &"))
|
|
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 & disown %1"))
|
|
wait_for_startup_headless()
|
|
wait_for_vm_boot_headless()
|
|
shutdown_vm_headless()
|
|
destroy_vm_headless()
|
|
'';
|
|
|
|
host-usb-permissions = ''
|
|
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 {})
|