at 22.05-pre 34 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, options, ... }: 11 12with lib; 13 14let 15 16 qemu-common = import ../../lib/qemu-common.nix { inherit lib pkgs; }; 17 18 cfg = config.virtualisation; 19 20 qemu = cfg.qemu.package; 21 22 consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles; 23 24 driveOpts = { ... }: { 25 26 options = { 27 28 file = mkOption { 29 type = types.str; 30 description = "The file image used for this drive."; 31 }; 32 33 driveExtraOpts = mkOption { 34 type = types.attrsOf types.str; 35 default = {}; 36 description = "Extra options passed to drive flag."; 37 }; 38 39 deviceExtraOpts = mkOption { 40 type = types.attrsOf types.str; 41 default = {}; 42 description = "Extra options passed to device flag."; 43 }; 44 45 name = mkOption { 46 type = types.nullOr types.str; 47 default = null; 48 description = 49 "A name for the drive. Must be unique in the drives list. Not passed to qemu."; 50 }; 51 52 }; 53 54 }; 55 56 driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }: 57 let 58 drvId = "drive${toString idx}"; 59 mkKeyValue = generators.mkKeyValueDefault {} "="; 60 mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts); 61 driveOpts = mkOpts (driveExtraOpts // { 62 index = idx; 63 id = drvId; 64 "if" = "none"; 65 inherit file; 66 }); 67 deviceOpts = mkOpts (deviceExtraOpts // { 68 drive = drvId; 69 }); 70 device = 71 if cfg.qemu.diskInterface == "scsi" then 72 "-device lsi53c895a -device scsi-hd,${deviceOpts}" 73 else 74 "-device virtio-blk-pci,${deviceOpts}"; 75 in 76 "-drive ${driveOpts} ${device}"; 77 78 drivesCmdLine = drives: concatStringsSep "\\\n " (imap1 driveCmdline drives); 79 80 81 # Creates a device name from a 1-based a numerical index, e.g. 82 # * `driveDeviceName 1` -> `/dev/vda` 83 # * `driveDeviceName 2` -> `/dev/vdb` 84 driveDeviceName = idx: 85 let letter = elemAt lowerChars (idx - 1); 86 in if cfg.qemu.diskInterface == "scsi" then 87 "/dev/sd${letter}" 88 else 89 "/dev/vd${letter}"; 90 91 lookupDriveDeviceName = driveName: driveList: 92 (findSingle (drive: drive.name == driveName) 93 (throw "Drive ${driveName} not found") 94 (throw "Multiple drives named ${driveName}") driveList).device; 95 96 addDeviceNames = 97 imap1 (idx: drive: drive // { device = driveDeviceName idx; }); 98 99 efiPrefix = 100 if pkgs.stdenv.hostPlatform.isx86 then "${pkgs.OVMF.fd}/FV/OVMF" 101 else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF" 102 else throw "No EFI firmware available for platform"; 103 efiFirmware = "${efiPrefix}_CODE.fd"; 104 efiVarsDefault = "${efiPrefix}_VARS.fd"; 105 106 # Shell script to start the VM. 107 startVM = 108 '' 109 #! ${pkgs.runtimeShell} 110 111 set -e 112 113 NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}}") 114 115 if ! test -e "$NIX_DISK_IMAGE"; then 116 ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \ 117 ${toString config.virtualisation.diskSize}M 118 fi 119 120 # Create a directory for storing temporary data of the running VM. 121 if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then 122 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir) 123 fi 124 125 ${lib.optionalString cfg.useNixStoreImage 126 '' 127 # Create a writable copy/snapshot of the store image. 128 ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${storeImage}/nixos.qcow2 "$TMPDIR"/store.img 129 ''} 130 131 # Create a directory for exchanging data with the VM. 132 mkdir -p "$TMPDIR/xchg" 133 134 ${lib.optionalString cfg.useBootLoader 135 '' 136 # Create a writable copy/snapshot of the boot disk. 137 # A writable boot disk can be booted from automatically. 138 ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${bootDisk}/disk.img "$TMPDIR/disk.img" 139 140 NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${cfg.efiVars}}") 141 142 ${lib.optionalString cfg.useEFIBoot 143 '' 144 # VM needs writable EFI vars 145 if ! test -e "$NIX_EFI_VARS"; then 146 cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" 147 chmod 0644 "$NIX_EFI_VARS" 148 fi 149 ''} 150 ''} 151 152 cd "$TMPDIR" 153 154 ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"} 155 ${flip concatMapStrings cfg.emptyDiskImages (size: '' 156 if ! test -e "empty$idx.qcow2"; then 157 ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M" 158 fi 159 idx=$((idx + 1)) 160 '')} 161 162 # Start QEMU. 163 exec ${qemu-common.qemuBinary qemu} \ 164 -name ${config.system.name} \ 165 -m ${toString config.virtualisation.memorySize} \ 166 -smp ${toString config.virtualisation.cores} \ 167 -device virtio-rng-pci \ 168 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \ 169 ${concatStringsSep " \\\n " 170 (mapAttrsToList 171 (tag: share: "-virtfs local,path=${share.source},security_model=none,mount_tag=${tag}") 172 config.virtualisation.sharedDirectories)} \ 173 ${drivesCmdLine config.virtualisation.qemu.drives} \ 174 ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \ 175 $QEMU_OPTS \ 176 "$@" 177 ''; 178 179 180 regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; }; 181 182 183 # Generate a hard disk image containing a /boot partition and GRUB 184 # in the MBR. Used when the `useBootLoader' option is set. 185 # Uses `runInLinuxVM` to create the image in a throwaway VM. 186 # See note [Disk layout with `useBootLoader`]. 187 # FIXME: use nixos/lib/make-disk-image.nix. 188 bootDisk = 189 pkgs.vmTools.runInLinuxVM ( 190 pkgs.runCommand "nixos-boot-disk" 191 { preVM = 192 '' 193 mkdir $out 194 diskImage=$out/disk.img 195 ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M" 196 ${if cfg.useEFIBoot then '' 197 efiVars=$out/efi-vars.fd 198 cp ${efiVarsDefault} $efiVars 199 chmod 0644 $efiVars 200 '' else ""} 201 ''; 202 buildInputs = [ pkgs.util-linux ]; 203 QEMU_OPTS = "-nographic -serial stdio -monitor none" 204 + lib.optionalString cfg.useEFIBoot ( 205 " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}" 206 + " -drive if=pflash,format=raw,unit=1,file=$efiVars"); 207 } 208 '' 209 # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility 210 ${pkgs.gptfdisk}/bin/sgdisk \ 211 --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \ 212 --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \ 213 --attributes=1:set:1 \ 214 --attributes=2:set:2 \ 215 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \ 216 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \ 217 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \ 218 --hybrid 2 \ 219 --recompute-chs /dev/vda 220 221 ${optionalString (config.boot.loader.grub.device != "/dev/vda") 222 # In this throwaway VM, we only have the /dev/vda disk, but the 223 # actual VM described by `config` (used by `switch-to-configuration` 224 # below) may set `boot.loader.grub.device` to a different device 225 # that's nonexistent in the throwaway VM. 226 # Create a symlink for that device, so that the `grub-install` 227 # by `switch-to-configuration` will hit /dev/vda anyway. 228 '' 229 ln -s /dev/vda ${config.boot.loader.grub.device} 230 '' 231 } 232 233 ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2 234 export MTOOLS_SKIP_CHECK=1 235 ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot 236 237 # Mount /boot; load necessary modules first. 238 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true 239 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true 240 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true 241 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true 242 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true 243 mkdir /boot 244 mount /dev/vda2 /boot 245 246 ${optionalString config.boot.loader.efi.canTouchEfiVariables '' 247 mount -t efivarfs efivarfs /sys/firmware/efi/efivars 248 ''} 249 250 # This is needed for GRUB 0.97, which doesn't know about virtio devices. 251 mkdir /boot/grub 252 echo '(hd0) /dev/vda' > /boot/grub/device.map 253 254 # This is needed for systemd-boot to find ESP, and udev is not available here to create this 255 mkdir -p /dev/block 256 ln -s /dev/vda2 /dev/block/254:2 257 258 # Set up system profile (normally done by nixos-rebuild / nix-env --set) 259 mkdir -p /nix/var/nix/profiles 260 ln -s ${config.system.build.toplevel} /nix/var/nix/profiles/system-1-link 261 ln -s /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system 262 263 # Install bootloader 264 touch /etc/NIXOS 265 export NIXOS_INSTALL_BOOTLOADER=1 266 ${config.system.build.toplevel}/bin/switch-to-configuration boot 267 268 umount /boot 269 '' # */ 270 ); 271 272 storeImage = import ../../lib/make-disk-image.nix { 273 inherit pkgs config lib; 274 additionalPaths = [ regInfo ]; 275 format = "qcow2"; 276 onlyNixStore = true; 277 partitionTableType = "none"; 278 installBootLoader = false; 279 diskSize = "auto"; 280 additionalSpace = "0M"; 281 copyChannel = false; 282 }; 283 284in 285 286{ 287 imports = [ 288 ../profiles/qemu-guest.nix 289 (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ]) 290 ]; 291 292 options = { 293 294 virtualisation.fileSystems = options.fileSystems; 295 296 virtualisation.memorySize = 297 mkOption { 298 type = types.ints.positive; 299 default = 1024; 300 description = 301 '' 302 The memory size in megabytes of the virtual machine. 303 ''; 304 }; 305 306 virtualisation.msize = 307 mkOption { 308 type = types.ints.positive; 309 default = pkgs.vmTools.default9PMsizeBytes; 310 description = 311 '' 312 The msize (maximum packet size) option passed to 9p file systems, in 313 bytes. Increasing this should increase performance significantly, 314 at the cost of higher RAM usage. 315 ''; 316 }; 317 318 virtualisation.diskSize = 319 mkOption { 320 type = types.nullOr types.ints.positive; 321 default = 1024; 322 description = 323 '' 324 The disk size in megabytes of the virtual machine. 325 ''; 326 }; 327 328 virtualisation.diskImage = 329 mkOption { 330 type = types.str; 331 default = "./${config.system.name}.qcow2"; 332 description = 333 '' 334 Path to the disk image containing the root filesystem. 335 The image will be created on startup if it does not 336 exist. 337 ''; 338 }; 339 340 virtualisation.bootDevice = 341 mkOption { 342 type = types.path; 343 example = "/dev/vda"; 344 description = 345 '' 346 The disk to be used for the root filesystem. 347 ''; 348 }; 349 350 virtualisation.emptyDiskImages = 351 mkOption { 352 type = types.listOf types.ints.positive; 353 default = []; 354 description = 355 '' 356 Additional disk images to provide to the VM. The value is 357 a list of size in megabytes of each disk. These disks are 358 writeable by the VM. 359 ''; 360 }; 361 362 virtualisation.graphics = 363 mkOption { 364 type = types.bool; 365 default = true; 366 description = 367 '' 368 Whether to run QEMU with a graphics window, or in nographic mode. 369 Serial console will be enabled on both settings, but this will 370 change the preferred console. 371 ''; 372 }; 373 374 virtualisation.resolution = 375 mkOption { 376 type = options.services.xserver.resolutions.type.nestedTypes.elemType; 377 default = { x = 1024; y = 768; }; 378 description = 379 '' 380 The resolution of the virtual machine display. 381 ''; 382 }; 383 384 virtualisation.cores = 385 mkOption { 386 type = types.ints.positive; 387 default = 1; 388 description = 389 '' 390 Specify the number of cores the guest is permitted to use. 391 The number can be higher than the available cores on the 392 host system. 393 ''; 394 }; 395 396 virtualisation.sharedDirectories = 397 mkOption { 398 type = types.attrsOf 399 (types.submodule { 400 options.source = mkOption { 401 type = types.str; 402 description = "The path of the directory to share, can be a shell variable"; 403 }; 404 options.target = mkOption { 405 type = types.path; 406 description = "The mount point of the directory inside the virtual machine"; 407 }; 408 }); 409 default = { }; 410 example = { 411 my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; }; 412 }; 413 description = 414 '' 415 An attributes set of directories that will be shared with the 416 virtual machine using VirtFS (9P filesystem over VirtIO). 417 The attribute name will be used as the 9P mount tag. 418 ''; 419 }; 420 421 virtualisation.additionalPaths = 422 mkOption { 423 type = types.listOf types.path; 424 default = []; 425 description = 426 '' 427 A list of paths whose closure should be made available to 428 the VM. 429 430 When 9p is used, the closure is registered in the Nix 431 database in the VM. All other paths in the host Nix store 432 appear in the guest Nix store as well, but are considered 433 garbage (because they are not registered in the Nix 434 database of the guest). 435 436 When <option>virtualisation.useNixStoreImage</option> is 437 set, the closure is copied to the Nix store image. 438 ''; 439 }; 440 441 virtualisation.forwardPorts = mkOption { 442 type = types.listOf 443 (types.submodule { 444 options.from = mkOption { 445 type = types.enum [ "host" "guest" ]; 446 default = "host"; 447 description = 448 '' 449 Controls the direction in which the ports are mapped: 450 451 - <literal>"host"</literal> means traffic from the host ports 452 is forwarded to the given guest port. 453 454 - <literal>"guest"</literal> means traffic from the guest ports 455 is forwarded to the given host port. 456 ''; 457 }; 458 options.proto = mkOption { 459 type = types.enum [ "tcp" "udp" ]; 460 default = "tcp"; 461 description = "The protocol to forward."; 462 }; 463 options.host.address = mkOption { 464 type = types.str; 465 default = ""; 466 description = "The IPv4 address of the host."; 467 }; 468 options.host.port = mkOption { 469 type = types.port; 470 description = "The host port to be mapped."; 471 }; 472 options.guest.address = mkOption { 473 type = types.str; 474 default = ""; 475 description = "The IPv4 address on the guest VLAN."; 476 }; 477 options.guest.port = mkOption { 478 type = types.port; 479 description = "The guest port to be mapped."; 480 }; 481 }); 482 default = []; 483 example = lib.literalExpression 484 '' 485 [ # forward local port 2222 -> 22, to ssh into the VM 486 { from = "host"; host.port = 2222; guest.port = 22; } 487 488 # forward local port 80 -> 10.0.2.10:80 in the VLAN 489 { from = "guest"; 490 guest.address = "10.0.2.10"; guest.port = 80; 491 host.address = "127.0.0.1"; host.port = 80; 492 } 493 ] 494 ''; 495 description = 496 '' 497 When using the SLiRP user networking (default), this option allows to 498 forward ports to/from the host/guest. 499 500 <warning><para> 501 If the NixOS firewall on the virtual machine is enabled, you also 502 have to open the guest ports to enable the traffic between host and 503 guest. 504 </para></warning> 505 506 <note><para>Currently QEMU supports only IPv4 forwarding.</para></note> 507 ''; 508 }; 509 510 virtualisation.vlans = 511 mkOption { 512 type = types.listOf types.ints.unsigned; 513 default = [ 1 ]; 514 example = [ 1 2 ]; 515 description = 516 '' 517 Virtual networks to which the VM is connected. Each 518 number <replaceable>N</replaceable> in this list causes 519 the VM to have a virtual Ethernet interface attached to a 520 separate virtual network on which it will be assigned IP 521 address 522 <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>, 523 where <replaceable>M</replaceable> is the index of this VM 524 in the list of VMs. 525 ''; 526 }; 527 528 virtualisation.writableStore = 529 mkOption { 530 type = types.bool; 531 default = true; # FIXME 532 description = 533 '' 534 If enabled, the Nix store in the VM is made writable by 535 layering an overlay filesystem on top of the host's Nix 536 store. 537 ''; 538 }; 539 540 virtualisation.writableStoreUseTmpfs = 541 mkOption { 542 type = types.bool; 543 default = true; 544 description = 545 '' 546 Use a tmpfs for the writable store instead of writing to the VM's 547 own filesystem. 548 ''; 549 }; 550 551 networking.primaryIPAddress = 552 mkOption { 553 type = types.str; 554 default = ""; 555 internal = true; 556 description = "Primary IP address used in /etc/hosts."; 557 }; 558 559 virtualisation.qemu = { 560 package = 561 mkOption { 562 type = types.package; 563 default = pkgs.qemu_kvm; 564 example = "pkgs.qemu_test"; 565 description = "QEMU package to use."; 566 }; 567 568 options = 569 mkOption { 570 type = types.listOf types.str; 571 default = []; 572 example = [ "-vga std" ]; 573 description = "Options passed to QEMU."; 574 }; 575 576 consoles = mkOption { 577 type = types.listOf types.str; 578 default = let 579 consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ]; 580 in if cfg.graphics then consoles else reverseList consoles; 581 example = [ "console=tty1" ]; 582 description = '' 583 The output console devices to pass to the kernel command line via the 584 <literal>console</literal> parameter, the primary console is the last 585 item of this list. 586 587 By default it enables both serial console and 588 <literal>tty0</literal>. The preferred console (last one) is based on 589 the value of <option>virtualisation.graphics</option>. 590 ''; 591 }; 592 593 networkingOptions = 594 mkOption { 595 type = types.listOf types.str; 596 default = [ ]; 597 example = [ 598 "-net nic,netdev=user.0,model=virtio" 599 "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" 600 ]; 601 description = '' 602 Networking-related command-line options that should be passed to qemu. 603 The default is to use userspace networking (SLiRP). 604 605 If you override this option, be advised to keep 606 ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example) 607 to keep the default runtime behaviour. 608 ''; 609 }; 610 611 drives = 612 mkOption { 613 type = types.listOf (types.submodule driveOpts); 614 description = "Drives passed to qemu."; 615 apply = addDeviceNames; 616 }; 617 618 diskInterface = 619 mkOption { 620 type = types.enum [ "virtio" "scsi" "ide" ]; 621 default = "virtio"; 622 example = "scsi"; 623 description = "The interface used for the virtual hard disks."; 624 }; 625 626 guestAgent.enable = 627 mkOption { 628 type = types.bool; 629 default = true; 630 description = '' 631 Enable the Qemu guest agent. 632 ''; 633 }; 634 }; 635 636 virtualisation.useNixStoreImage = 637 mkOption { 638 type = types.bool; 639 default = false; 640 description = '' 641 Build and use a disk image for the Nix store, instead of 642 accessing the host's one through 9p. 643 644 For applications which do a lot of reads from the store, 645 this can drastically improve performance, but at the cost of 646 disk space and image build time. 647 ''; 648 }; 649 650 virtualisation.useBootLoader = 651 mkOption { 652 type = types.bool; 653 default = false; 654 description = 655 '' 656 If enabled, the virtual machine will be booted using the 657 regular boot loader (i.e., GRUB 1 or 2). This allows 658 testing of the boot loader. If 659 disabled (the default), the VM directly boots the NixOS 660 kernel and initial ramdisk, bypassing the boot loader 661 altogether. 662 ''; 663 }; 664 665 virtualisation.useEFIBoot = 666 mkOption { 667 type = types.bool; 668 default = false; 669 description = 670 '' 671 If enabled, the virtual machine will provide a EFI boot 672 manager. 673 useEFIBoot is ignored if useBootLoader == false. 674 ''; 675 }; 676 677 virtualisation.efiVars = 678 mkOption { 679 type = types.str; 680 default = "./${config.system.name}-efi-vars.fd"; 681 description = 682 '' 683 Path to nvram image containing UEFI variables. The will be created 684 on startup if it does not exist. 685 ''; 686 }; 687 688 virtualisation.bios = 689 mkOption { 690 type = types.nullOr types.package; 691 default = null; 692 description = 693 '' 694 An alternate BIOS (such as <package>qboot</package>) with which to start the VM. 695 Should contain a file named <literal>bios.bin</literal>. 696 If <literal>null</literal>, QEMU's builtin SeaBIOS will be used. 697 ''; 698 }; 699 700 }; 701 702 config = { 703 704 assertions = 705 lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule: 706 [ 707 { assertion = rule.from == "guest" -> rule.proto == "tcp"; 708 message = 709 '' 710 Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto: 711 Guest forwarding supports only TCP connections. 712 ''; 713 } 714 { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address; 715 message = 716 '' 717 Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address: 718 The address must be in the default VLAN (10.0.2.0/24). 719 ''; 720 } 721 ])); 722 723 # Note [Disk layout with `useBootLoader`] 724 # 725 # If `useBootLoader = true`, we configure 2 drives: 726 # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk 727 # which has the `/boot` partition and the boot loader. 728 # Concretely: 729 # 730 # * The second drive's image `disk.img` is created in `bootDisk = ...` 731 # using a throwaway VM. Note that there the disk is always `/dev/vda`, 732 # even though in the final VM it will be at `/dev/*b`. 733 # * The disks are attached in `virtualisation.qemu.drives`. 734 # Their order makes them appear as devices `a`, `b`, etc. 735 # * `fileSystems."/boot"` is adjusted to be on device `b`. 736 737 # If `useBootLoader`, GRUB goes to the second disk, see 738 # note [Disk layout with `useBootLoader`]. 739 boot.loader.grub.device = mkVMOverride ( 740 if cfg.useBootLoader 741 then driveDeviceName 2 # second disk 742 else cfg.bootDevice 743 ); 744 boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}"; 745 746 boot.initrd.extraUtilsCommands = 747 '' 748 # We need mke2fs in the initrd. 749 copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs 750 ''; 751 752 boot.initrd.postDeviceCommands = 753 '' 754 # If the disk image appears to be empty, run mke2fs to 755 # initialise. 756 FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true) 757 if test -z "$FSTYPE"; then 758 mke2fs -t ext4 ${cfg.bootDevice} 759 fi 760 ''; 761 762 boot.initrd.postMountCommands = 763 '' 764 # Mark this as a NixOS machine. 765 mkdir -p $targetRoot/etc 766 echo -n > $targetRoot/etc/NIXOS 767 768 # Fix the permissions on /tmp. 769 chmod 1777 $targetRoot/tmp 770 771 mkdir -p $targetRoot/boot 772 773 ${optionalString cfg.writableStore '' 774 echo "mounting overlay filesystem on /nix/store..." 775 mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store 776 mount -t overlay overlay $targetRoot/nix/store \ 777 -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail 778 ''} 779 ''; 780 781 # After booting, register the closure of the paths in 782 # `virtualisation.additionalPaths' in the Nix database in the VM. This 783 # allows Nix operations to work in the VM. The path to the 784 # registration file is passed through the kernel command line to 785 # allow `system.build.toplevel' to be included. (If we had a direct 786 # reference to ${regInfo} here, then we would get a cyclic 787 # dependency.) 788 boot.postBootCommands = 789 '' 790 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then 791 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} 792 fi 793 ''; 794 795 boot.initrd.availableKernelModules = 796 optional cfg.writableStore "overlay" 797 ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"; 798 799 virtualisation.bootDevice = mkDefault (driveDeviceName 1); 800 801 virtualisation.additionalPaths = [ config.system.build.toplevel ]; 802 803 virtualisation.sharedDirectories = { 804 nix-store = mkIf (!cfg.useNixStoreImage) { 805 source = builtins.storeDir; 806 target = "/nix/store"; 807 }; 808 xchg = { 809 source = ''"$TMPDIR"/xchg''; 810 target = "/tmp/xchg"; 811 }; 812 shared = { 813 source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"''; 814 target = "/tmp/shared"; 815 }; 816 }; 817 818 virtualisation.qemu.networkingOptions = 819 let 820 forwardingOptions = flip concatMapStrings cfg.forwardPorts 821 ({ proto, from, host, guest }: 822 if from == "host" 823 then "hostfwd=${proto}:${host.address}:${toString host.port}-" + 824 "${guest.address}:${toString guest.port}," 825 else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" + 826 "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}'," 827 ); 828 in 829 [ 830 "-net nic,netdev=user.0,model=virtio" 831 "-netdev user,id=user.0,${forwardingOptions}\"$QEMU_NET_OPTS\"" 832 ]; 833 834 # FIXME: Consolidate this one day. 835 virtualisation.qemu.options = mkMerge [ 836 (mkIf pkgs.stdenv.hostPlatform.isx86 [ 837 "-usb" "-device usb-tablet,bus=usb-bus.0" 838 ]) 839 (mkIf (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [ 840 "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet" 841 ]) 842 (mkIf (!cfg.useBootLoader) [ 843 "-kernel ${config.system.build.toplevel}/kernel" 844 "-initrd ${config.system.build.toplevel}/initrd" 845 ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' 846 ]) 847 (mkIf cfg.useEFIBoot [ 848 "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}" 849 "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS" 850 ]) 851 (mkIf (cfg.bios != null) [ 852 "-bios ${cfg.bios}/bios.bin" 853 ]) 854 (mkIf (!cfg.graphics) [ 855 "-nographic" 856 ]) 857 ]; 858 859 virtualisation.qemu.drives = mkMerge [ 860 [{ 861 name = "root"; 862 file = ''"$NIX_DISK_IMAGE"''; 863 driveExtraOpts.cache = "writeback"; 864 driveExtraOpts.werror = "report"; 865 }] 866 (mkIf cfg.useNixStoreImage [{ 867 name = "nix-store"; 868 file = ''"$TMPDIR"/store.img''; 869 deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2"; 870 }]) 871 (mkIf cfg.useBootLoader [ 872 # The order of this list determines the device names, see 873 # note [Disk layout with `useBootLoader`]. 874 { 875 name = "boot"; 876 file = ''"$TMPDIR"/disk.img''; 877 driveExtraOpts.media = "disk"; 878 deviceExtraOpts.bootindex = "1"; 879 } 880 ]) 881 (imap0 (idx: _: { 882 file = "$(pwd)/empty${toString idx}.qcow2"; 883 driveExtraOpts.werror = "report"; 884 }) cfg.emptyDiskImages) 885 ]; 886 887 # Mount the host filesystem via 9P, and bind-mount the Nix store 888 # of the host into our own filesystem. We use mkVMOverride to 889 # allow this module to be applied to "normal" NixOS system 890 # configuration, where the regular value for the `fileSystems' 891 # attribute should be disregarded for the purpose of building a VM 892 # test image (since those filesystems don't exist in the VM). 893 fileSystems = 894 let 895 mkSharedDir = tag: share: 896 { 897 name = 898 if tag == "nix-store" && cfg.writableStore 899 then "/nix/.ro-store" 900 else share.target; 901 value.device = tag; 902 value.fsType = "9p"; 903 value.neededForBoot = true; 904 value.options = 905 [ "trans=virtio" "version=9p2000.L" "msize=${toString cfg.msize}" ] 906 ++ lib.optional (tag == "nix-store") "cache=loose"; 907 }; 908 in 909 mkVMOverride (cfg.fileSystems // 910 { 911 "/".device = cfg.bootDevice; 912 913 "/tmp" = mkIf config.boot.tmpOnTmpfs 914 { device = "tmpfs"; 915 fsType = "tmpfs"; 916 neededForBoot = true; 917 # Sync with systemd's tmp.mount; 918 options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ]; 919 }; 920 921 "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = 922 mkIf cfg.useNixStoreImage 923 { device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}"; 924 neededForBoot = true; 925 options = [ "ro" ]; 926 }; 927 928 "/nix/.rw-store" = mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) 929 { fsType = "tmpfs"; 930 options = [ "mode=0755" ]; 931 neededForBoot = true; 932 }; 933 934 "/boot" = mkIf cfg.useBootLoader 935 # see note [Disk layout with `useBootLoader`] 936 { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk` 937 fsType = "vfat"; 938 noCheck = true; # fsck fails on a r/o filesystem 939 }; 940 } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories); 941 942 swapDevices = mkVMOverride [ ]; 943 boot.initrd.luks.devices = mkVMOverride {}; 944 945 # Don't run ntpd in the guest. It should get the correct time from KVM. 946 services.timesyncd.enable = false; 947 948 services.qemuGuest.enable = cfg.qemu.guestAgent.enable; 949 950 system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; } 951 '' 952 mkdir -p $out/bin 953 ln -s ${config.system.build.toplevel} $out/system 954 ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm 955 ''; 956 957 # When building a regular system configuration, override whatever 958 # video driver the host uses. 959 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ]; 960 services.xserver.defaultDepth = mkVMOverride 0; 961 services.xserver.resolutions = mkVMOverride [ cfg.resolution ]; 962 services.xserver.monitorSection = 963 '' 964 # Set a higher refresh rate so that resolutions > 800x600 work. 965 HorizSync 30-140 966 VertRefresh 50-160 967 ''; 968 969 # Wireless won't work in the VM. 970 networking.wireless.enable = mkVMOverride false; 971 services.connman.enable = mkVMOverride false; 972 973 # Speed up booting by not waiting for ARP. 974 networking.dhcpcd.extraConfig = "noarp"; 975 976 networking.usePredictableInterfaceNames = false; 977 978 system.requiredKernelConfig = with config.lib.kernelConfig; 979 [ (isEnabled "VIRTIO_BLK") 980 (isEnabled "VIRTIO_PCI") 981 (isEnabled "VIRTIO_NET") 982 (isEnabled "EXT4_FS") 983 (isEnabled "NET_9P_VIRTIO") 984 (isEnabled "9P_FS") 985 (isYes "BLK_DEV") 986 (isYes "PCI") 987 (isYes "NETDEVICES") 988 (isYes "NET_CORE") 989 (isYes "INET") 990 (isYes "NETWORK_FILESYSTEMS") 991 ] ++ optionals (!cfg.graphics) [ 992 (isYes "SERIAL_8250_CONSOLE") 993 (isYes "SERIAL_8250") 994 ] ++ optionals (cfg.writableStore) [ 995 (isEnabled "OVERLAY_FS") 996 ]; 997 998 }; 999}