at v206 13 kB view raw
1{ debug ? false, ... } @ args: 2 3import ./make-test.nix ({ pkgs, ... }: with pkgs.lib; let 4 5 testVMConfig = vmName: attrs: { config, pkgs, ... }: let 6 guestAdditions = pkgs.linuxPackages.virtualboxGuestAdditions; 7 8 miniInit = '' 9 #!${pkgs.stdenv.shell} -xe 10 export PATH="${pkgs.coreutils}/bin:${pkgs.utillinux}/bin" 11 12 mkdir -p /etc/dbus-1 /var/run/dbus 13 cat > /etc/passwd <<EOF 14 root:x:0:0::/root:/bin/false 15 messagebus:x:1:1::/var/run/dbus:/bin/false 16 EOF 17 cat > /etc/group <<EOF 18 root:x:0: 19 messagebus:x:1: 20 EOF 21 cp -v "${pkgs.dbus.daemon}/etc/dbus-1/system.conf" \ 22 /etc/dbus-1/system.conf 23 "${pkgs.dbus.daemon}/bin/dbus-daemon" --fork --system 24 25 ${guestAdditions}/bin/VBoxService 26 ${(attrs.vmScript or (const "")) pkgs} 27 28 i=0 29 while [ ! -e /mnt-root/shutdown ]; do 30 sleep 10 31 i=$(($i + 10)) 32 [ $i -le 120 ] || fail 33 done 34 35 rm -f /mnt-root/boot-done /mnt-root/shutdown 36 ''; 37 in { 38 boot.kernelParams = [ 39 "console=tty0" "console=ttyS0" "ignore_loglevel" 40 "boot.trace" "panic=1" "boot.panic_on_fail" 41 "init=${pkgs.writeScript "mini-init.sh" miniInit}" 42 ]; 43 44 fileSystems."/" = { 45 device = "vboxshare"; 46 fsType = "vboxsf"; 47 }; 48 49 virtualisation.virtualbox.guest.enable = true; 50 51 boot.initrd.kernelModules = [ 52 "af_packet" "vboxsf" 53 "virtio" "virtio_pci" "virtio_ring" "virtio_net" "vboxguest" 54 ]; 55 56 boot.initrd.extraUtilsCommands = '' 57 copy_bin_and_libs "${guestAdditions}/bin/mount.vboxsf" 58 copy_bin_and_libs "${pkgs.utillinux}/bin/unshare" 59 ${(attrs.extraUtilsCommands or (const "")) pkgs} 60 ''; 61 62 boot.initrd.postMountCommands = '' 63 touch /mnt-root/boot-done 64 hostname "${vmName}" 65 mkdir -p /nix/store 66 unshare -m "@shell@" -c ' 67 mount -t vboxsf nixstore /nix/store 68 exec "$stage2Init" 69 ' 70 poweroff -f 71 ''; 72 73 system.requiredKernelConfig = with config.lib.kernelConfig; [ 74 (isYes "SERIAL_8250_CONSOLE") 75 (isYes "SERIAL_8250") 76 ]; 77 }; 78 79 mkLog = logfile: tag: let 80 rotated = map (i: "${logfile}.${toString i}") (range 1 9); 81 all = concatMapStringsSep " " (f: "\"${f}\"") ([logfile] ++ rotated); 82 logcmd = "tail -F ${all} 2> /dev/null | logger -t \"${tag}\""; 83 in optionalString debug "$machine->execute(ru '${logcmd} & disown');"; 84 85 testVM = vmName: vmScript: let 86 cfg = (import ../lib/eval-config.nix { 87 system = "i686-linux"; 88 modules = [ 89 ../modules/profiles/minimal.nix 90 (testVMConfig vmName vmScript) 91 ]; 92 }).config; 93 in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" { 94 preVM = '' 95 mkdir -p "$out" 96 diskImage="$(pwd)/qimage" 97 ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M 98 ''; 99 100 postVM = '' 101 echo "creating VirtualBox disk image..." 102 ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \ 103 "$diskImage" "$out/disk.vdi" 104 ''; 105 106 buildInputs = [ pkgs.utillinux pkgs.perl ]; 107 } '' 108 ${pkgs.parted}/sbin/parted /dev/vda mklabel msdos 109 ${pkgs.parted}/sbin/parted /dev/vda -- mkpart primary ext2 1M -1s 110 . /sys/class/block/vda1/uevent 111 mknod /dev/vda1 b $MAJOR $MINOR 112 113 ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1 114 ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1 115 mkdir /mnt 116 mount /dev/vda1 /mnt 117 cp "${cfg.system.build.kernel}/bzImage" /mnt/linux 118 cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd 119 120 ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda 121 122 cat > /mnt/grub/grub.cfg <<GRUB 123 set root=hd0,1 124 linux /linux ${concatStringsSep " " cfg.boot.kernelParams} 125 initrd /initrd 126 boot 127 GRUB 128 umount /mnt 129 ''); 130 131 createVM = name: attrs: let 132 mkFlags = concatStringsSep " "; 133 134 sharePath = "/home/alice/vboxshare-${name}"; 135 136 createFlags = mkFlags [ 137 "--ostype Linux26" 138 "--register" 139 ]; 140 141 vmFlags = mkFlags ([ 142 "--uart1 0x3F8 4" 143 "--uartmode1 client /run/virtualbox-log-${name}.sock" 144 "--memory 768" 145 ] ++ (attrs.vmFlags or [])); 146 147 controllerFlags = mkFlags [ 148 "--name SATA" 149 "--add sata" 150 "--bootable on" 151 "--hostiocache on" 152 ]; 153 154 diskFlags = mkFlags [ 155 "--storagectl SATA" 156 "--port 0" 157 "--device 0" 158 "--type hdd" 159 "--mtype immutable" 160 "--medium ${testVM name attrs}/disk.vdi" 161 ]; 162 163 sharedFlags = mkFlags [ 164 "--name vboxshare" 165 "--hostpath ${sharePath}" 166 ]; 167 168 nixstoreFlags = mkFlags [ 169 "--name nixstore" 170 "--hostpath /nix/store" 171 "--readonly" 172 ]; 173 in { 174 machine = { 175 systemd.sockets."vboxtestlog-${name}" = { 176 description = "VirtualBox Test Machine Log Socket For ${name}"; 177 wantedBy = [ "sockets.target" ]; 178 before = [ "multi-user.target" ]; 179 socketConfig.ListenStream = "/run/virtualbox-log-${name}.sock"; 180 socketConfig.Accept = true; 181 }; 182 183 systemd.services."vboxtestlog-${name}@" = { 184 description = "VirtualBox Test Machine Log For ${name}"; 185 serviceConfig.StandardInput = "socket"; 186 serviceConfig.StandardOutput = "syslog"; 187 serviceConfig.SyslogIdentifier = "GUEST-${name}"; 188 serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat"; 189 }; 190 }; 191 192 testSubs = '' 193 my ${"$" + name}_sharepath = '${sharePath}'; 194 195 sub checkRunning_${name} { 196 my $cmd = 'VBoxManage list runningvms | grep -q "^\"${name}\""'; 197 my ($status, $out) = $machine->execute(ru $cmd); 198 return $status == 0; 199 } 200 201 sub cleanup_${name} { 202 $machine->execute(ru "VBoxManage controlvm ${name} poweroff") 203 if checkRunning_${name}; 204 $machine->succeed("rm -rf ${sharePath}"); 205 $machine->succeed("mkdir -p ${sharePath}"); 206 $machine->succeed("chown alice.users ${sharePath}"); 207 } 208 209 sub createVM_${name} { 210 vbm("createvm --name ${name} ${createFlags}"); 211 vbm("modifyvm ${name} ${vmFlags}"); 212 vbm("setextradata ${name} VBoxInternal/PDM/HaltOnReset 1"); 213 vbm("storagectl ${name} ${controllerFlags}"); 214 vbm("storageattach ${name} ${diskFlags}"); 215 vbm("sharedfolder add ${name} ${sharedFlags}"); 216 vbm("sharedfolder add ${name} ${nixstoreFlags}"); 217 cleanup_${name}; 218 219 ${mkLog "$HOME/VirtualBox VMs/${name}/Logs/VBox.log" "HOST-${name}"} 220 } 221 222 sub destroyVM_${name} { 223 cleanup_${name}; 224 vbm("unregistervm ${name} --delete"); 225 } 226 227 sub waitForVMBoot_${name} { 228 $machine->execute(ru( 229 'set -e; i=0; '. 230 'while ! test -e ${sharePath}/boot-done; do '. 231 'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '. 232 'VBoxManage list runningvms | grep -q "^\"${name}\""; '. 233 'done' 234 )); 235 } 236 237 sub waitForIP_${name} ($) { 238 my $property = "/VirtualBox/GuestInfo/Net/$_[0]/V4/IP"; 239 my $getip = "VBoxManage guestproperty get ${name} $property | ". 240 "sed -n -e 's/^Value: //p'"; 241 my $ip = $machine->succeed(ru( 242 'for i in $(seq 1000); do '. 243 'if ipaddr="$('.$getip.')" && [ -n "$ipaddr" ]; then '. 244 'echo "$ipaddr"; exit 0; '. 245 'fi; '. 246 'sleep 1; '. 247 'done; '. 248 'echo "Could not get IPv4 address for ${name}!" >&2; '. 249 'exit 1' 250 )); 251 chomp $ip; 252 return $ip; 253 } 254 255 sub waitForStartup_${name} { 256 for (my $i = 0; $i <= 120; $i += 10) { 257 $machine->sleep(10); 258 return if checkRunning_${name}; 259 eval { $_[0]->() } if defined $_[0]; 260 } 261 die "VirtualBox VM didn't start up within 2 minutes"; 262 } 263 264 sub waitForShutdown_${name} { 265 for (my $i = 0; $i <= 120; $i += 10) { 266 $machine->sleep(10); 267 return unless checkRunning_${name}; 268 } 269 die "VirtualBox VM didn't shut down within 2 minutes"; 270 } 271 272 sub shutdownVM_${name} { 273 $machine->succeed(ru "touch ${sharePath}/shutdown"); 274 $machine->waitUntilSucceeds( 275 "test ! -e ${sharePath}/shutdown ". 276 " -a ! -e ${sharePath}/boot-done" 277 ); 278 waitForShutdown_${name}; 279 } 280 ''; 281 }; 282 283 hostonlyVMFlags = [ 284 "--nictype1 virtio" 285 "--nictype2 virtio" 286 "--nic2 hostonly" 287 "--hostonlyadapter2 vboxnet0" 288 ]; 289 290 dhcpScript = pkgs: '' 291 ${pkgs.dhcp}/bin/dhclient \ 292 -lf /run/dhcp.leases \ 293 -pf /run/dhclient.pid \ 294 -v eth0 eth1 295 296 otherIP="$(${pkgs.netcat}/bin/netcat -clp 1234 || :)" 297 ${pkgs.iputils}/bin/ping -I eth1 -c1 "$otherIP" 298 echo "$otherIP reachable" | ${pkgs.netcat}/bin/netcat -clp 5678 || : 299 ''; 300 301 sysdDetectVirt = pkgs: '' 302 ${pkgs.systemd}/bin/systemd-detect-virt > /mnt-root/result 303 ''; 304 305 vboxVMs = mapAttrs createVM { 306 simple = {}; 307 308 detectvirt.vmScript = sysdDetectVirt; 309 310 test1.vmFlags = hostonlyVMFlags; 311 test1.vmScript = dhcpScript; 312 313 test2.vmFlags = hostonlyVMFlags; 314 test2.vmScript = dhcpScript; 315 }; 316 317in { 318 name = "virtualbox"; 319 meta = with pkgs.stdenv.lib.maintainers; { 320 maintainers = [ aszlig wkennington ]; 321 }; 322 323 machine = { pkgs, lib, config, ... }: { 324 imports = let 325 mkVMConf = name: val: val.machine // { key = "${name}-config"; }; 326 vmConfigs = mapAttrsToList mkVMConf vboxVMs; 327 in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs; 328 virtualisation.memorySize = 2048; 329 virtualisation.virtualbox.host.enable = true; 330 users.extraUsers.alice.extraGroups = let 331 inherit (config.virtualisation.virtualbox.host) enableHardening; 332 in lib.mkIf enableHardening (lib.singleton "vboxusers"); 333 }; 334 335 testScript = '' 336 sub ru ($) { 337 my $esc = $_[0] =~ s/'/'\\${"'"}'/gr; 338 return "su - alice -c '$esc'"; 339 } 340 341 sub vbm { 342 $machine->succeed(ru("VBoxManage ".$_[0])); 343 }; 344 345 ${concatStrings (mapAttrsToList (_: getAttr "testSubs") vboxVMs)} 346 347 $machine->waitForX; 348 349 ${mkLog "$HOME/.config/VirtualBox/VBoxSVC.log" "HOST-SVC"} 350 351 createVM_simple; 352 353 subtest "simple-gui", sub { 354 $machine->succeed(ru "VirtualBox &"); 355 $machine->waitForWindow(qr/Oracle VM VirtualBox Manager/); 356 $machine->sleep(5); 357 $machine->screenshot("gui_manager_started"); 358 $machine->sendKeys("ret"); 359 $machine->screenshot("gui_manager_sent_startup"); 360 waitForStartup_simple (sub { 361 $machine->sendKeys("ret"); 362 }); 363 $machine->screenshot("gui_started"); 364 waitForVMBoot_simple; 365 $machine->screenshot("gui_booted"); 366 shutdownVM_simple; 367 $machine->sleep(5); 368 $machine->screenshot("gui_stopped"); 369 $machine->sendKeys("ctrl-q"); 370 $machine->sleep(5); 371 $machine->screenshot("gui_manager_stopped"); 372 }; 373 374 cleanup_simple; 375 376 subtest "simple-cli", sub { 377 vbm("startvm simple"); 378 waitForStartup_simple; 379 $machine->screenshot("cli_started"); 380 waitForVMBoot_simple; 381 $machine->screenshot("cli_booted"); 382 shutdownVM_simple; 383 }; 384 385 subtest "privilege-escalation", sub { 386 $machine->fail("test -e '/root/VirtualBox VMs'"); 387 $machine->fail("test -e '/root/.config/VirtualBox'"); 388 $machine->succeed("test -e '/home/alice/VirtualBox VMs'"); 389 }; 390 391 destroyVM_simple; 392 393 sub removeUUIDs { 394 return join("\n", grep { $_ !~ /^UUID:/ } split(/\n/, $_[0]))."\n"; 395 } 396 397 subtest "host-usb-permissions", sub { 398 my $userUSB = removeUUIDs vbm("list usbhost"); 399 print STDERR $userUSB; 400 my $rootUSB = removeUUIDs $machine->succeed("VBoxManage list usbhost"); 401 print STDERR $rootUSB; 402 403 die "USB host devices differ for root and normal user" 404 if $userUSB ne $rootUSB; 405 die "No USB host devices found" if $userUSB =~ /<none>/; 406 }; 407 408 subtest "systemd-detect-virt", sub { 409 createVM_detectvirt; 410 vbm("startvm detectvirt"); 411 waitForStartup_detectvirt; 412 waitForVMBoot_detectvirt; 413 shutdownVM_detectvirt; 414 my $result = $machine->succeed("cat '$detectvirt_sharepath/result'"); 415 chomp $result; 416 destroyVM_detectvirt; 417 die "systemd-detect-virt returned \"$result\" instead of \"oracle\"" 418 if $result ne "oracle"; 419 }; 420 421 subtest "net-hostonlyif", sub { 422 createVM_test1; 423 createVM_test2; 424 425 vbm("startvm test1"); 426 waitForStartup_test1; 427 waitForVMBoot_test1; 428 429 vbm("startvm test2"); 430 waitForStartup_test2; 431 waitForVMBoot_test2; 432 433 $machine->screenshot("net_booted"); 434 435 my $test1IP = waitForIP_test1 1; 436 my $test2IP = waitForIP_test2 1; 437 438 $machine->succeed("echo '$test2IP' | netcat -c '$test1IP' 1234"); 439 $machine->succeed("echo '$test1IP' | netcat -c '$test2IP' 1234"); 440 441 $machine->waitUntilSucceeds("netcat -c '$test1IP' 5678 >&2"); 442 $machine->waitUntilSucceeds("netcat -c '$test2IP' 5678 >&2"); 443 444 shutdownVM_test1; 445 shutdownVM_test2; 446 447 destroyVM_test1; 448 destroyVM_test2; 449 }; 450 ''; 451}) args