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