at 17.09-beta 19 kB view raw
1# This module creates a virtual machine from the NixOS configuration. 2# Building the `config.system.build.vm' attribute gives you a command 3# that starts a KVM/QEMU VM running the NixOS configuration defined in 4# `config'. The Nix store is shared read-only with the host, which 5# makes (re)building VMs very efficient. However, it also means you 6# can't reconfigure the guest inside the guest - you need to rebuild 7# the VM in the host. On the other hand, the root filesystem is a 8# read/writable disk image persistent across VM reboots. 9 10{ config, lib, pkgs, ... }: 11 12with lib; 13 14let 15 16 qemu = config.system.build.qemu or pkgs.qemu_test; 17 18 vmName = 19 if config.networking.hostName == "" 20 then "noname" 21 else config.networking.hostName; 22 23 cfg = config.virtualisation; 24 25 qemuGraphics = if cfg.graphics then "" else "-nographic"; 26 kernelConsole = if cfg.graphics then "" else "console=ttyS0"; 27 ttys = [ "tty1" "tty2" "tty3" "tty4" "tty5" "tty6" ]; 28 29 # Shell script to start the VM. 30 startVM = 31 '' 32 #! ${pkgs.stdenv.shell} 33 34 NIX_DISK_IMAGE=$(readlink -f ''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}}) 35 36 if ! test -e "$NIX_DISK_IMAGE"; then 37 ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \ 38 ${toString config.virtualisation.diskSize}M || exit 1 39 fi 40 41 # Create a directory for storing temporary data of the running VM. 42 if [ -z "$TMPDIR" -o -z "$USE_TMPDIR" ]; then 43 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir) 44 fi 45 46 # Create a directory for exchanging data with the VM. 47 mkdir -p $TMPDIR/xchg 48 49 ${if cfg.useBootLoader then '' 50 # Create a writable copy/snapshot of the boot disk. 51 # A writable boot disk can be booted from automatically. 52 ${qemu}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1 53 54 ${if cfg.useEFIBoot then '' 55 # VM needs a writable flash BIOS. 56 cp ${bootDisk}/bios.bin $TMPDIR || exit 1 57 chmod 0644 $TMPDIR/bios.bin || exit 1 58 '' else '' 59 ''} 60 '' else '' 61 ''} 62 63 cd $TMPDIR 64 idx=2 65 extraDisks="" 66 ${flip concatMapStrings cfg.emptyDiskImages (size: '' 67 if ! test -e "empty$idx.qcow2"; then 68 ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M" 69 fi 70 extraDisks="$extraDisks -drive index=$idx,file=$(pwd)/empty$idx.qcow2,if=${cfg.qemu.diskInterface},werror=report" 71 idx=$((idx + 1)) 72 '')} 73 74 # Start QEMU. 75 exec ${qemu}/bin/qemu-kvm \ 76 -name ${vmName} \ 77 -m ${toString config.virtualisation.memorySize} \ 78 -smp ${toString config.virtualisation.cores} \ 79 ${optionalString (pkgs.stdenv.system == "x86_64-linux") "-cpu kvm64"} \ 80 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \ 81 -virtfs local,path=/nix/store,security_model=none,mount_tag=store \ 82 -virtfs local,path=$TMPDIR/xchg,security_model=none,mount_tag=xchg \ 83 -virtfs local,path=''${SHARED_DIR:-$TMPDIR/xchg},security_model=none,mount_tag=shared \ 84 ${if cfg.useBootLoader then '' 85 -drive index=0,id=drive1,file=$NIX_DISK_IMAGE,if=${cfg.qemu.diskInterface},cache=writeback,werror=report \ 86 -drive index=1,id=drive2,file=$TMPDIR/disk.img,media=disk \ 87 ${if cfg.useEFIBoot then '' 88 -pflash $TMPDIR/bios.bin \ 89 '' else '' 90 ''} 91 '' else '' 92 -drive index=0,id=drive1,file=$NIX_DISK_IMAGE,if=${cfg.qemu.diskInterface},cache=writeback,werror=report \ 93 -kernel ${config.system.build.toplevel}/kernel \ 94 -initrd ${config.system.build.toplevel}/initrd \ 95 -append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo} ${kernelConsole} $QEMU_KERNEL_PARAMS" \ 96 ''} \ 97 $extraDisks \ 98 ${qemuGraphics} \ 99 ${toString config.virtualisation.qemu.options} \ 100 $QEMU_OPTS \ 101 $@ 102 ''; 103 104 105 regInfo = pkgs.runCommand "reginfo" 106 { exportReferencesGraph = 107 map (x: [("closure-" + baseNameOf x) x]) config.virtualisation.pathsInNixDB; 108 buildInputs = [ pkgs.perl ]; 109 preferLocalBuild = true; 110 } 111 '' 112 printRegistration=1 perl ${pkgs.pathsFromGraph} closure-* > $out 113 ''; 114 115 116 # Generate a hard disk image containing a /boot partition and GRUB 117 # in the MBR. Used when the `useBootLoader' option is set. 118 # FIXME: use nixos/lib/make-disk-image.nix. 119 bootDisk = 120 pkgs.vmTools.runInLinuxVM ( 121 pkgs.runCommand "nixos-boot-disk" 122 { preVM = 123 '' 124 mkdir $out 125 diskImage=$out/disk.img 126 bootFlash=$out/bios.bin 127 ${qemu}/bin/qemu-img create -f qcow2 $diskImage "40M" 128 ${if cfg.useEFIBoot then '' 129 cp ${pkgs.OVMF-CSM.fd}/FV/OVMF.fd $bootFlash 130 chmod 0644 $bootFlash 131 '' else '' 132 ''} 133 ''; 134 buildInputs = [ pkgs.utillinux ]; 135 QEMU_OPTS = if cfg.useEFIBoot 136 then "-pflash $out/bios.bin -nographic -serial pty" 137 else "-nographic -serial pty"; 138 } 139 '' 140 # Create a /boot EFI partition with 40M and arbitrary but fixed GUIDs for reproducibility 141 ${pkgs.gptfdisk}/bin/sgdisk \ 142 --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \ 143 --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \ 144 --attributes=1:set:1 \ 145 --attributes=2:set:2 \ 146 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \ 147 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \ 148 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \ 149 --hybrid 2 \ 150 --recompute-chs /dev/vda 151 . /sys/class/block/vda2/uevent 152 mknod /dev/vda2 b $MAJOR $MINOR 153 . /sys/class/block/vda/uevent 154 ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2 155 export MTOOLS_SKIP_CHECK=1 156 ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot 157 158 # Mount /boot; load necessary modules first. 159 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true 160 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true 161 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true 162 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true 163 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true 164 mkdir /boot 165 mount /dev/vda2 /boot 166 167 # This is needed for GRUB 0.97, which doesn't know about virtio devices. 168 mkdir /boot/grub 169 echo '(hd0) /dev/vda' > /boot/grub/device.map 170 171 # Install GRUB and generate the GRUB boot menu. 172 touch /etc/NIXOS 173 mkdir -p /nix/var/nix/profiles 174 ${config.system.build.toplevel}/bin/switch-to-configuration boot 175 176 umount /boot 177 '' # */ 178 ); 179 180in 181 182{ 183 imports = [ ../profiles/qemu-guest.nix ]; 184 185 options = { 186 187 virtualisation.memorySize = 188 mkOption { 189 default = 384; 190 description = 191 '' 192 Memory size (M) of virtual machine. 193 ''; 194 }; 195 196 virtualisation.diskSize = 197 mkOption { 198 default = 512; 199 description = 200 '' 201 Disk size (M) of virtual machine. 202 ''; 203 }; 204 205 virtualisation.diskImage = 206 mkOption { 207 default = "./${vmName}.qcow2"; 208 description = 209 '' 210 Path to the disk image containing the root filesystem. 211 The image will be created on startup if it does not 212 exist. 213 ''; 214 }; 215 216 virtualisation.bootDevice = 217 mkOption { 218 type = types.str; 219 example = "/dev/vda"; 220 description = 221 '' 222 The disk to be used for the root filesystem. 223 ''; 224 }; 225 226 virtualisation.emptyDiskImages = 227 mkOption { 228 default = []; 229 type = types.listOf types.int; 230 description = 231 '' 232 Additional disk images to provide to the VM. The value is 233 a list of size in megabytes of each disk. These disks are 234 writeable by the VM. 235 ''; 236 }; 237 238 virtualisation.graphics = 239 mkOption { 240 default = true; 241 description = 242 '' 243 Whether to run QEMU with a graphics window, or access 244 the guest computer serial port through the host tty. 245 ''; 246 }; 247 248 virtualisation.cores = 249 mkOption { 250 default = 1; 251 type = types.int; 252 description = 253 '' 254 Specify the number of cores the guest is permitted to use. 255 The number can be higher than the available cores on the 256 host system. 257 ''; 258 }; 259 260 virtualisation.pathsInNixDB = 261 mkOption { 262 default = []; 263 description = 264 '' 265 The list of paths whose closure is registered in the Nix 266 database in the VM. All other paths in the host Nix store 267 appear in the guest Nix store as well, but are considered 268 garbage (because they are not registered in the Nix 269 database in the guest). 270 ''; 271 }; 272 273 virtualisation.vlans = 274 mkOption { 275 default = [ 1 ]; 276 example = [ 1 2 ]; 277 description = 278 '' 279 Virtual networks to which the VM is connected. Each 280 number <replaceable>N</replaceable> in this list causes 281 the VM to have a virtual Ethernet interface attached to a 282 separate virtual network on which it will be assigned IP 283 address 284 <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>, 285 where <replaceable>M</replaceable> is the index of this VM 286 in the list of VMs. 287 ''; 288 }; 289 290 virtualisation.writableStore = 291 mkOption { 292 default = true; # FIXME 293 description = 294 '' 295 If enabled, the Nix store in the VM is made writable by 296 layering an overlay filesystem on top of the host's Nix 297 store. 298 ''; 299 }; 300 301 virtualisation.writableStoreUseTmpfs = 302 mkOption { 303 default = true; 304 description = 305 '' 306 Use a tmpfs for the writable store instead of writing to the VM's 307 own filesystem. 308 ''; 309 }; 310 311 networking.primaryIPAddress = 312 mkOption { 313 default = ""; 314 internal = true; 315 description = "Primary IP address used in /etc/hosts."; 316 }; 317 318 virtualisation.qemu = { 319 options = 320 mkOption { 321 type = types.listOf types.unspecified; 322 default = []; 323 example = [ "-vga std" ]; 324 description = "Options passed to QEMU."; 325 }; 326 327 networkingOptions = 328 mkOption { 329 default = [ 330 "-net nic,vlan=0,model=virtio" 331 "-net user,vlan=0\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" 332 ]; 333 type = types.listOf types.str; 334 description = '' 335 Networking-related command-line options that should be passed to qemu. 336 The default is to use userspace networking (slirp). 337 338 If you override this option, be advised to keep 339 ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the default) 340 to keep the default runtime behaviour. 341 ''; 342 }; 343 344 diskInterface = 345 mkOption { 346 default = "virtio"; 347 example = "scsi"; 348 type = types.str; 349 description = '' 350 The interface used for the virtual hard disks 351 (<literal>virtio</literal> or <literal>scsi</literal>). 352 ''; 353 }; 354 }; 355 356 virtualisation.useBootLoader = 357 mkOption { 358 default = false; 359 description = 360 '' 361 If enabled, the virtual machine will be booted using the 362 regular boot loader (i.e., GRUB 1 or 2). This allows 363 testing of the boot loader. If 364 disabled (the default), the VM directly boots the NixOS 365 kernel and initial ramdisk, bypassing the boot loader 366 altogether. 367 ''; 368 }; 369 370 virtualisation.useEFIBoot = 371 mkOption { 372 default = false; 373 description = 374 '' 375 If enabled, the virtual machine will provide a EFI boot 376 manager. 377 useEFIBoot is ignored if useBootLoader == false. 378 ''; 379 }; 380 381 }; 382 383 config = { 384 385 boot.loader.grub.device = mkVMOverride cfg.bootDevice; 386 387 boot.initrd.extraUtilsCommands = 388 '' 389 # We need mke2fs in the initrd. 390 copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs 391 ''; 392 393 boot.initrd.postDeviceCommands = 394 '' 395 # If the disk image appears to be empty, run mke2fs to 396 # initialise. 397 FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true) 398 if test -z "$FSTYPE"; then 399 mke2fs -t ext4 ${cfg.bootDevice} 400 fi 401 ''; 402 403 boot.initrd.postMountCommands = 404 '' 405 # Mark this as a NixOS machine. 406 mkdir -p $targetRoot/etc 407 echo -n > $targetRoot/etc/NIXOS 408 409 # Fix the permissions on /tmp. 410 chmod 1777 $targetRoot/tmp 411 412 mkdir -p $targetRoot/boot 413 414 ${optionalString cfg.writableStore '' 415 echo "mounting overlay filesystem on /nix/store..." 416 mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store 417 mount -t overlay overlay $targetRoot/nix/store \ 418 -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail 419 ''} 420 ''; 421 422 # After booting, register the closure of the paths in 423 # `virtualisation.pathsInNixDB' in the Nix database in the VM. This 424 # allows Nix operations to work in the VM. The path to the 425 # registration file is passed through the kernel command line to 426 # allow `system.build.toplevel' to be included. (If we had a direct 427 # reference to ${regInfo} here, then we would get a cyclic 428 # dependency.) 429 boot.postBootCommands = 430 '' 431 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then 432 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} 433 fi 434 ''; 435 436 boot.initrd.availableKernelModules = 437 optional cfg.writableStore "overlay" 438 ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"; 439 440 virtualisation.bootDevice = 441 mkDefault (if cfg.qemu.diskInterface == "scsi" then "/dev/sda" else "/dev/vda"); 442 443 virtualisation.pathsInNixDB = [ config.system.build.toplevel ]; 444 445 virtualisation.qemu.options = [ "-vga std" "-usbdevice tablet" ]; 446 447 # Mount the host filesystem via 9P, and bind-mount the Nix store 448 # of the host into our own filesystem. We use mkVMOverride to 449 # allow this module to be applied to "normal" NixOS system 450 # configuration, where the regular value for the `fileSystems' 451 # attribute should be disregarded for the purpose of building a VM 452 # test image (since those filesystems don't exist in the VM). 453 fileSystems = mkVMOverride ( 454 { "/".device = cfg.bootDevice; 455 ${if cfg.writableStore then "/nix/.ro-store" else "/nix/store"} = 456 { device = "store"; 457 fsType = "9p"; 458 options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ]; 459 neededForBoot = true; 460 }; 461 "/tmp" = mkIf config.boot.tmpOnTmpfs 462 { device = "tmpfs"; 463 fsType = "tmpfs"; 464 neededForBoot = true; 465 # Sync with systemd's tmp.mount; 466 options = [ "mode=1777" "strictatime" "nosuid" "nodev" ]; 467 }; 468 "/tmp/xchg" = 469 { device = "xchg"; 470 fsType = "9p"; 471 options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ]; 472 neededForBoot = true; 473 }; 474 "/tmp/shared" = 475 { device = "shared"; 476 fsType = "9p"; 477 options = [ "trans=virtio" "version=9p2000.L" ]; 478 neededForBoot = true; 479 }; 480 } // optionalAttrs (cfg.writableStore && cfg.writableStoreUseTmpfs) 481 { "/nix/.rw-store" = 482 { fsType = "tmpfs"; 483 options = [ "mode=0755" ]; 484 neededForBoot = true; 485 }; 486 } // optionalAttrs cfg.useBootLoader 487 { "/boot" = 488 { device = "/dev/vdb2"; 489 fsType = "vfat"; 490 options = [ "ro" ]; 491 noCheck = true; # fsck fails on a r/o filesystem 492 }; 493 }); 494 495 swapDevices = mkVMOverride [ ]; 496 boot.initrd.luks.devices = mkVMOverride {}; 497 498 # Don't run ntpd in the guest. It should get the correct time from KVM. 499 services.timesyncd.enable = false; 500 501 system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; } 502 '' 503 mkdir -p $out/bin 504 ln -s ${config.system.build.toplevel} $out/system 505 ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${vmName}-vm 506 ''; 507 508 # When building a regular system configuration, override whatever 509 # video driver the host uses. 510 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ]; 511 services.xserver.defaultDepth = mkVMOverride 0; 512 services.xserver.resolutions = mkVMOverride [ { x = 1024; y = 768; } ]; 513 services.xserver.monitorSection = 514 '' 515 # Set a higher refresh rate so that resolutions > 800x600 work. 516 HorizSync 30-140 517 VertRefresh 50-160 518 ''; 519 520 # Wireless won't work in the VM. 521 networking.wireless.enable = mkVMOverride false; 522 networking.connman.enable = mkVMOverride false; 523 524 # Speed up booting by not waiting for ARP. 525 networking.dhcpcd.extraConfig = "noarp"; 526 527 networking.usePredictableInterfaceNames = false; 528 529 system.requiredKernelConfig = with config.lib.kernelConfig; 530 [ (isEnabled "VIRTIO_BLK") 531 (isEnabled "VIRTIO_PCI") 532 (isEnabled "VIRTIO_NET") 533 (isEnabled "EXT4_FS") 534 (isYes "BLK_DEV") 535 (isYes "PCI") 536 (isYes "EXPERIMENTAL") 537 (isYes "NETDEVICES") 538 (isYes "NET_CORE") 539 (isYes "INET") 540 (isYes "NETWORK_FILESYSTEMS") 541 ] ++ optional (!cfg.graphics) [ 542 (isYes "SERIAL_8250_CONSOLE") 543 (isYes "SERIAL_8250") 544 ]; 545 546 }; 547}