at 23.11-pre 43 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 opt = options.virtualisation; 21 22 qemu = cfg.qemu.package; 23 24 consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles; 25 26 driveOpts = { ... }: { 27 28 options = { 29 30 file = mkOption { 31 type = types.str; 32 description = lib.mdDoc "The file image used for this drive."; 33 }; 34 35 driveExtraOpts = mkOption { 36 type = types.attrsOf types.str; 37 default = {}; 38 description = lib.mdDoc "Extra options passed to drive flag."; 39 }; 40 41 deviceExtraOpts = mkOption { 42 type = types.attrsOf types.str; 43 default = {}; 44 description = lib.mdDoc "Extra options passed to device flag."; 45 }; 46 47 name = mkOption { 48 type = types.nullOr types.str; 49 default = null; 50 description = 51 lib.mdDoc "A name for the drive. Must be unique in the drives list. Not passed to qemu."; 52 }; 53 54 }; 55 56 }; 57 58 selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }: 59 if useDefaultFilesystems then 60 if useEFIBoot then "efi" else "legacy" 61 else "none"; 62 63 driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }: 64 let 65 drvId = "drive${toString idx}"; 66 mkKeyValue = generators.mkKeyValueDefault {} "="; 67 mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts); 68 driveOpts = mkOpts (driveExtraOpts // { 69 index = idx; 70 id = drvId; 71 "if" = "none"; 72 inherit file; 73 }); 74 deviceOpts = mkOpts (deviceExtraOpts // { 75 drive = drvId; 76 }); 77 device = 78 if cfg.qemu.diskInterface == "scsi" then 79 "-device lsi53c895a -device scsi-hd,${deviceOpts}" 80 else 81 "-device virtio-blk-pci,${deviceOpts}"; 82 in 83 "-drive ${driveOpts} ${device}"; 84 85 drivesCmdLine = drives: concatStringsSep "\\\n " (imap1 driveCmdline drives); 86 87 88 # Creates a device name from a 1-based a numerical index, e.g. 89 # * `driveDeviceName 1` -> `/dev/vda` 90 # * `driveDeviceName 2` -> `/dev/vdb` 91 driveDeviceName = idx: 92 let letter = elemAt lowerChars (idx - 1); 93 in if cfg.qemu.diskInterface == "scsi" then 94 "/dev/sd${letter}" 95 else 96 "/dev/vd${letter}"; 97 98 lookupDriveDeviceName = driveName: driveList: 99 (findSingle (drive: drive.name == driveName) 100 (throw "Drive ${driveName} not found") 101 (throw "Multiple drives named ${driveName}") driveList).device; 102 103 addDeviceNames = 104 imap1 (idx: drive: drive // { device = driveDeviceName idx; }); 105 106 # Shell script to start the VM. 107 startVM = 108 '' 109 #! ${cfg.host.pkgs.runtimeShell} 110 111 export PATH=${makeBinPath [ cfg.host.pkgs.coreutils ]}''${PATH:+:}$PATH 112 113 set -e 114 115 NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE" 116 117 if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then 118 echo "Disk image do not exist, creating the virtualisation disk image..." 119 # If we are using a bootloader and default filesystems layout. 120 # We have to reuse the system image layout as a backing image format (CoW) 121 # So we can write on the top of it. 122 123 # If we are not using the default FS layout, potentially, we are interested into 124 # performing operations in postDeviceCommands or at early boot on the raw device. 125 # We can still boot through QEMU direct kernel boot feature. 126 127 # CoW prevent size to be attributed to an image. 128 # FIXME: raise this issue to upstream. 129 ${qemu}/bin/qemu-img create \ 130 ${concatStringsSep " \\\n" ([ "-f qcow2" ] 131 ++ optional (cfg.useBootLoader && cfg.useDefaultFilesystems) "-F qcow2 -b ${systemImage}/nixos.qcow2" 132 ++ optional (!(cfg.useBootLoader && cfg.useDefaultFilesystems)) "-o size=${toString config.virtualisation.diskSize}M" 133 ++ [ ''"$NIX_DISK_IMAGE"'' ])} 134 echo "Virtualisation disk image created." 135 fi 136 137 # Create a directory for storing temporary data of the running VM. 138 if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then 139 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir) 140 fi 141 142 ${lib.optionalString (cfg.useNixStoreImage) 143 (if cfg.writableStore 144 then '' 145 # Create a writable copy/snapshot of the store image. 146 ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${storeImage}/nixos.qcow2 "$TMPDIR"/store.img 147 '' 148 else '' 149 ( 150 cd ${builtins.storeDir} 151 ${pkgs.erofs-utils}/bin/mkfs.erofs \ 152 --force-uid=0 \ 153 --force-gid=0 \ 154 -U eb176051-bd15-49b7-9e6b-462e0b467019 \ 155 -T 0 \ 156 --exclude-regex="$( 157 <${pkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \ 158 sed -e 's^.*/^^g' \ 159 | cut -c -10 \ 160 | ${pkgs.python3}/bin/python ${./includes-to-excludes.py} )" \ 161 "$TMPDIR"/store.img \ 162 . \ 163 </dev/null >/dev/null 164 ) 165 '' 166 ) 167 } 168 169 # Create a directory for exchanging data with the VM. 170 mkdir -p "$TMPDIR/xchg" 171 172 ${lib.optionalString cfg.useEFIBoot 173 '' 174 # Expose EFI variables, it's useful even when we are not using a bootloader (!). 175 # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence 176 # no guard against `useBootLoader`. Examples: 177 # - testing PXE boot or other EFI applications 178 # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows 179 NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}") 180 # VM needs writable EFI vars 181 if ! test -e "$NIX_EFI_VARS"; then 182 ${if cfg.useBootLoader then 183 # We still need the EFI var from the make-disk-image derivation 184 # because our "switch-to-configuration" process might 185 # write into it and we want to keep this data. 186 ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"'' 187 else 188 ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"'' 189 } 190 chmod 0644 "$NIX_EFI_VARS" 191 fi 192 ''} 193 194 cd "$TMPDIR" 195 196 ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"} 197 ${flip concatMapStrings cfg.emptyDiskImages (size: '' 198 if ! test -e "empty$idx.qcow2"; then 199 ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M" 200 fi 201 idx=$((idx + 1)) 202 '')} 203 204 # Start QEMU. 205 exec ${qemu-common.qemuBinary qemu} \ 206 -name ${config.system.name} \ 207 -m ${toString config.virtualisation.memorySize} \ 208 -smp ${toString config.virtualisation.cores} \ 209 -device virtio-rng-pci \ 210 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \ 211 ${concatStringsSep " \\\n " 212 (mapAttrsToList 213 (tag: share: "-virtfs local,path=${share.source},security_model=none,mount_tag=${tag}") 214 config.virtualisation.sharedDirectories)} \ 215 ${drivesCmdLine config.virtualisation.qemu.drives} \ 216 ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \ 217 $QEMU_OPTS \ 218 "$@" 219 ''; 220 221 222 regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; }; 223 224 # System image is akin to a complete NixOS install with 225 # a boot partition and root partition. 226 systemImage = import ../../lib/make-disk-image.nix { 227 inherit pkgs config lib; 228 additionalPaths = [ regInfo ]; 229 format = "qcow2"; 230 onlyNixStore = false; 231 partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; }; 232 # Bootloader should be installed on the system image only if we are booting through bootloaders. 233 # Though, if a user is not using our default filesystems, it is possible to not have any ESP 234 # or a strange partition table that's incompatible with GRUB configuration. 235 # As a consequence, this may lead to disk image creation failures. 236 # To avoid this, we prefer to let the user find out about how to install the bootloader on its ESP/disk. 237 # Usually, this can be through building your own disk image. 238 # TODO: If a user is interested into a more fine grained heuristic for `installBootLoader` 239 # by examining the actual contents of `cfg.fileSystems`, please send a PR. 240 installBootLoader = cfg.useBootLoader && cfg.useDefaultFilesystems; 241 touchEFIVars = cfg.useEFIBoot; 242 diskSize = "auto"; 243 additionalSpace = "0M"; 244 copyChannel = false; 245 OVMF = cfg.efi.OVMF; 246 }; 247 248 storeImage = import ../../lib/make-disk-image.nix { 249 inherit pkgs config lib; 250 additionalPaths = [ regInfo ]; 251 format = "qcow2"; 252 onlyNixStore = true; 253 partitionTableType = "none"; 254 installBootLoader = false; 255 touchEFIVars = false; 256 diskSize = "auto"; 257 additionalSpace = "0M"; 258 copyChannel = false; 259 }; 260 261 bootConfiguration = 262 if cfg.useDefaultFilesystems 263 then 264 if cfg.useBootLoader 265 then 266 if cfg.useEFIBoot then "efi_bootloading_with_default_fs" 267 else "legacy_bootloading_with_default_fs" 268 else 269 "direct_boot_with_default_fs" 270 else 271 "custom"; 272 suggestedRootDevice = { 273 "efi_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}2"; 274 "legacy_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}1"; 275 "direct_boot_with_default_fs" = cfg.bootLoaderDevice; 276 # This will enforce a NixOS module type checking error 277 # to ask explicitly the user to set a rootDevice. 278 # As it will look like `rootDevice = lib.mkDefault null;` after 279 # all "computations". 280 "custom" = null; 281 }.${bootConfiguration}; 282in 283 284{ 285 imports = [ 286 ../profiles/qemu-guest.nix 287 (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ]) 288 (mkRemovedOptionModule [ "virtualisation" "bootDevice" ] "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues.") 289 (mkRemovedOptionModule [ "virtualisation" "efiVars" ] "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue") 290 (mkRemovedOptionModule [ "virtualisation" "persistBootDevice" ] "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`") 291 ]; 292 293 options = { 294 295 virtualisation.fileSystems = options.fileSystems; 296 297 virtualisation.memorySize = 298 mkOption { 299 type = types.ints.positive; 300 default = 1024; 301 description = 302 lib.mdDoc '' 303 The memory size in megabytes of the virtual machine. 304 ''; 305 }; 306 307 virtualisation.msize = 308 mkOption { 309 type = types.ints.positive; 310 default = 16384; 311 description = 312 lib.mdDoc '' 313 The msize (maximum packet size) option passed to 9p file systems, in 314 bytes. Increasing this should increase performance significantly, 315 at the cost of higher RAM usage. 316 ''; 317 }; 318 319 virtualisation.diskSize = 320 mkOption { 321 type = types.nullOr types.ints.positive; 322 default = 1024; 323 description = 324 lib.mdDoc '' 325 The disk size in megabytes of the virtual machine. 326 ''; 327 }; 328 329 virtualisation.diskImage = 330 mkOption { 331 type = types.nullOr types.str; 332 default = "./${config.system.name}.qcow2"; 333 defaultText = literalExpression ''"./''${config.system.name}.qcow2"''; 334 description = 335 lib.mdDoc '' 336 Path to the disk image containing the root filesystem. 337 The image will be created on startup if it does not 338 exist. 339 340 If null, a tmpfs will be used as the root filesystem and 341 the VM's state will not be persistent. 342 ''; 343 }; 344 345 virtualisation.bootLoaderDevice = 346 mkOption { 347 type = types.path; 348 default = lookupDriveDeviceName "root" cfg.qemu.drives; 349 defaultText = literalExpression ''lookupDriveDeviceName "root" cfg.qemu.drives''; 350 example = "/dev/vda"; 351 description = 352 lib.mdDoc '' 353 The disk to be used for the boot filesystem. 354 By default, it is the same disk as the root filesystem. 355 ''; 356 }; 357 358 virtualisation.bootPartition = 359 mkOption { 360 type = types.nullOr types.path; 361 default = if cfg.useEFIBoot then "${cfg.bootLoaderDevice}1" else null; 362 defaultText = literalExpression ''if cfg.useEFIBoot then "''${cfg.bootLoaderDevice}1" else null''; 363 example = "/dev/vda1"; 364 description = 365 lib.mdDoc '' 366 The boot partition to be used to mount /boot filesystem. 367 In legacy boots, this should be null. 368 By default, in EFI boot, it is the first partition of the boot device. 369 ''; 370 }; 371 372 virtualisation.rootDevice = 373 mkOption { 374 type = types.nullOr types.path; 375 example = "/dev/vda2"; 376 description = 377 lib.mdDoc '' 378 The disk or partition to be used for the root filesystem. 379 By default (read the source code for more details): 380 381 - under EFI with a bootloader: 2nd partition of the boot disk 382 - in legacy boot with a bootloader: 1st partition of the boot disk 383 - in direct boot (i.e. without a bootloader): whole disk 384 385 In case you are not using a default boot device or a default filesystem, you have to set explicitly your root device. 386 ''; 387 }; 388 389 virtualisation.emptyDiskImages = 390 mkOption { 391 type = types.listOf types.ints.positive; 392 default = []; 393 description = 394 lib.mdDoc '' 395 Additional disk images to provide to the VM. The value is 396 a list of size in megabytes of each disk. These disks are 397 writeable by the VM. 398 ''; 399 }; 400 401 virtualisation.graphics = 402 mkOption { 403 type = types.bool; 404 default = true; 405 description = 406 lib.mdDoc '' 407 Whether to run QEMU with a graphics window, or in nographic mode. 408 Serial console will be enabled on both settings, but this will 409 change the preferred console. 410 ''; 411 }; 412 413 virtualisation.resolution = 414 mkOption { 415 type = options.services.xserver.resolutions.type.nestedTypes.elemType; 416 default = { x = 1024; y = 768; }; 417 description = 418 lib.mdDoc '' 419 The resolution of the virtual machine display. 420 ''; 421 }; 422 423 virtualisation.cores = 424 mkOption { 425 type = types.ints.positive; 426 default = 1; 427 description = 428 lib.mdDoc '' 429 Specify the number of cores the guest is permitted to use. 430 The number can be higher than the available cores on the 431 host system. 432 ''; 433 }; 434 435 virtualisation.sharedDirectories = 436 mkOption { 437 type = types.attrsOf 438 (types.submodule { 439 options.source = mkOption { 440 type = types.str; 441 description = lib.mdDoc "The path of the directory to share, can be a shell variable"; 442 }; 443 options.target = mkOption { 444 type = types.path; 445 description = lib.mdDoc "The mount point of the directory inside the virtual machine"; 446 }; 447 }); 448 default = { }; 449 example = { 450 my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; }; 451 }; 452 description = 453 lib.mdDoc '' 454 An attributes set of directories that will be shared with the 455 virtual machine using VirtFS (9P filesystem over VirtIO). 456 The attribute name will be used as the 9P mount tag. 457 ''; 458 }; 459 460 virtualisation.additionalPaths = 461 mkOption { 462 type = types.listOf types.path; 463 default = []; 464 description = 465 lib.mdDoc '' 466 A list of paths whose closure should be made available to 467 the VM. 468 469 When 9p is used, the closure is registered in the Nix 470 database in the VM. All other paths in the host Nix store 471 appear in the guest Nix store as well, but are considered 472 garbage (because they are not registered in the Nix 473 database of the guest). 474 475 When {option}`virtualisation.useNixStoreImage` is 476 set, the closure is copied to the Nix store image. 477 ''; 478 }; 479 480 virtualisation.forwardPorts = mkOption { 481 type = types.listOf 482 (types.submodule { 483 options.from = mkOption { 484 type = types.enum [ "host" "guest" ]; 485 default = "host"; 486 description = 487 lib.mdDoc '' 488 Controls the direction in which the ports are mapped: 489 490 - `"host"` means traffic from the host ports 491 is forwarded to the given guest port. 492 - `"guest"` means traffic from the guest ports 493 is forwarded to the given host port. 494 ''; 495 }; 496 options.proto = mkOption { 497 type = types.enum [ "tcp" "udp" ]; 498 default = "tcp"; 499 description = lib.mdDoc "The protocol to forward."; 500 }; 501 options.host.address = mkOption { 502 type = types.str; 503 default = ""; 504 description = lib.mdDoc "The IPv4 address of the host."; 505 }; 506 options.host.port = mkOption { 507 type = types.port; 508 description = lib.mdDoc "The host port to be mapped."; 509 }; 510 options.guest.address = mkOption { 511 type = types.str; 512 default = ""; 513 description = lib.mdDoc "The IPv4 address on the guest VLAN."; 514 }; 515 options.guest.port = mkOption { 516 type = types.port; 517 description = lib.mdDoc "The guest port to be mapped."; 518 }; 519 }); 520 default = []; 521 example = lib.literalExpression 522 '' 523 [ # forward local port 2222 -> 22, to ssh into the VM 524 { from = "host"; host.port = 2222; guest.port = 22; } 525 526 # forward local port 80 -> 10.0.2.10:80 in the VLAN 527 { from = "guest"; 528 guest.address = "10.0.2.10"; guest.port = 80; 529 host.address = "127.0.0.1"; host.port = 80; 530 } 531 ] 532 ''; 533 description = 534 lib.mdDoc '' 535 When using the SLiRP user networking (default), this option allows to 536 forward ports to/from the host/guest. 537 538 ::: {.warning} 539 If the NixOS firewall on the virtual machine is enabled, you also 540 have to open the guest ports to enable the traffic between host and 541 guest. 542 ::: 543 544 ::: {.note} 545 Currently QEMU supports only IPv4 forwarding. 546 ::: 547 ''; 548 }; 549 550 virtualisation.restrictNetwork = 551 mkOption { 552 type = types.bool; 553 default = false; 554 example = true; 555 description = 556 lib.mdDoc '' 557 If this option is enabled, the guest will be isolated, i.e. it will 558 not be able to contact the host and no guest IP packets will be 559 routed over the host to the outside. This option does not affect 560 any explicitly set forwarding rules. 561 ''; 562 }; 563 564 virtualisation.vlans = 565 mkOption { 566 type = types.listOf types.ints.unsigned; 567 default = [ 1 ]; 568 example = [ 1 2 ]; 569 description = 570 lib.mdDoc '' 571 Virtual networks to which the VM is connected. Each 572 number «N» in this list causes 573 the VM to have a virtual Ethernet interface attached to a 574 separate virtual network on which it will be assigned IP 575 address 576 `192.168.«N».«M»`, 577 where «M» is the index of this VM 578 in the list of VMs. 579 ''; 580 }; 581 582 virtualisation.writableStore = 583 mkOption { 584 type = types.bool; 585 default = cfg.mountHostNixStore; 586 defaultText = literalExpression "cfg.mountHostNixStore"; 587 description = 588 lib.mdDoc '' 589 If enabled, the Nix store in the VM is made writable by 590 layering an overlay filesystem on top of the host's Nix 591 store. 592 593 By default, this is enabled if you mount a host Nix store. 594 ''; 595 }; 596 597 virtualisation.writableStoreUseTmpfs = 598 mkOption { 599 type = types.bool; 600 default = true; 601 description = 602 lib.mdDoc '' 603 Use a tmpfs for the writable store instead of writing to the VM's 604 own filesystem. 605 ''; 606 }; 607 608 networking.primaryIPAddress = 609 mkOption { 610 type = types.str; 611 default = ""; 612 internal = true; 613 description = lib.mdDoc "Primary IP address used in /etc/hosts."; 614 }; 615 616 virtualisation.host.pkgs = mkOption { 617 type = options.nixpkgs.pkgs.type; 618 default = pkgs; 619 defaultText = literalExpression "pkgs"; 620 example = literalExpression '' 621 import pkgs.path { system = "x86_64-darwin"; } 622 ''; 623 description = lib.mdDoc '' 624 pkgs set to use for the host-specific packages of the vm runner. 625 Changing this to e.g. a Darwin package set allows running NixOS VMs on Darwin. 626 ''; 627 }; 628 629 virtualisation.qemu = { 630 package = 631 mkOption { 632 type = types.package; 633 default = cfg.host.pkgs.qemu_kvm; 634 defaultText = literalExpression "config.virtualisation.host.pkgs.qemu_kvm"; 635 example = literalExpression "pkgs.qemu_test"; 636 description = lib.mdDoc "QEMU package to use."; 637 }; 638 639 options = 640 mkOption { 641 type = types.listOf types.str; 642 default = []; 643 example = [ "-vga std" ]; 644 description = lib.mdDoc "Options passed to QEMU."; 645 }; 646 647 consoles = mkOption { 648 type = types.listOf types.str; 649 default = let 650 consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ]; 651 in if cfg.graphics then consoles else reverseList consoles; 652 example = [ "console=tty1" ]; 653 description = lib.mdDoc '' 654 The output console devices to pass to the kernel command line via the 655 `console` parameter, the primary console is the last 656 item of this list. 657 658 By default it enables both serial console and 659 `tty0`. The preferred console (last one) is based on 660 the value of {option}`virtualisation.graphics`. 661 ''; 662 }; 663 664 networkingOptions = 665 mkOption { 666 type = types.listOf types.str; 667 default = [ ]; 668 example = [ 669 "-net nic,netdev=user.0,model=virtio" 670 "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" 671 ]; 672 description = lib.mdDoc '' 673 Networking-related command-line options that should be passed to qemu. 674 The default is to use userspace networking (SLiRP). 675 676 If you override this option, be advised to keep 677 ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example) 678 to keep the default runtime behaviour. 679 ''; 680 }; 681 682 drives = 683 mkOption { 684 type = types.listOf (types.submodule driveOpts); 685 description = lib.mdDoc "Drives passed to qemu."; 686 apply = addDeviceNames; 687 }; 688 689 diskInterface = 690 mkOption { 691 type = types.enum [ "virtio" "scsi" "ide" ]; 692 default = "virtio"; 693 example = "scsi"; 694 description = lib.mdDoc "The interface used for the virtual hard disks."; 695 }; 696 697 guestAgent.enable = 698 mkOption { 699 type = types.bool; 700 default = true; 701 description = lib.mdDoc '' 702 Enable the Qemu guest agent. 703 ''; 704 }; 705 706 virtioKeyboard = 707 mkOption { 708 type = types.bool; 709 default = true; 710 description = lib.mdDoc '' 711 Enable the virtio-keyboard device. 712 ''; 713 }; 714 }; 715 716 virtualisation.useNixStoreImage = 717 mkOption { 718 type = types.bool; 719 default = false; 720 description = lib.mdDoc '' 721 Build and use a disk image for the Nix store, instead of 722 accessing the host's one through 9p. 723 724 For applications which do a lot of reads from the store, 725 this can drastically improve performance, but at the cost of 726 disk space and image build time. 727 728 As an alternative, you can use a bootloader which will provide you 729 with a full NixOS system image containing a Nix store and 730 avoid mounting the host nix store through 731 {option}`virtualisation.mountHostNixStore`. 732 ''; 733 }; 734 735 virtualisation.mountHostNixStore = 736 mkOption { 737 type = types.bool; 738 default = !cfg.useNixStoreImage && !cfg.useBootLoader; 739 defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader"; 740 description = lib.mdDoc '' 741 Mount the host Nix store as a 9p mount. 742 ''; 743 }; 744 745 virtualisation.useBootLoader = 746 mkOption { 747 type = types.bool; 748 default = false; 749 description = 750 lib.mdDoc '' 751 If enabled, the virtual machine will be booted using the 752 regular boot loader (i.e., GRUB 1 or 2). This allows 753 testing of the boot loader. If 754 disabled (the default), the VM directly boots the NixOS 755 kernel and initial ramdisk, bypassing the boot loader 756 altogether. 757 ''; 758 }; 759 760 virtualisation.useEFIBoot = 761 mkOption { 762 type = types.bool; 763 default = false; 764 description = 765 lib.mdDoc '' 766 If enabled, the virtual machine will provide a EFI boot 767 manager. 768 useEFIBoot is ignored if useBootLoader == false. 769 ''; 770 }; 771 772 virtualisation.efi = { 773 OVMF = mkOption { 774 type = types.package; 775 default = (pkgs.OVMF.override { 776 secureBoot = cfg.useSecureBoot; 777 }).fd; 778 defaultText = ''(pkgs.OVMF.override { 779 secureBoot = cfg.useSecureBoot; 780 }).fd''; 781 description = 782 lib.mdDoc "OVMF firmware package, defaults to OVMF configured with secure boot if needed."; 783 }; 784 785 firmware = mkOption { 786 type = types.path; 787 default = cfg.efi.OVMF.firmware; 788 defaultText = literalExpression "cfg.efi.OVMF.firmware"; 789 description = 790 lib.mdDoc '' 791 Firmware binary for EFI implementation, defaults to OVMF. 792 ''; 793 }; 794 795 variables = mkOption { 796 type = types.path; 797 default = cfg.efi.OVMF.variables; 798 defaultText = literalExpression "cfg.efi.OVMF.variables"; 799 description = 800 lib.mdDoc '' 801 Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware. 802 Defaults to OVMF. 803 ''; 804 }; 805 }; 806 807 virtualisation.useDefaultFilesystems = 808 mkOption { 809 type = types.bool; 810 default = true; 811 description = 812 lib.mdDoc '' 813 If enabled, the boot disk of the virtual machine will be 814 formatted and mounted with the default filesystems for 815 testing. Swap devices and LUKS will be disabled. 816 817 If disabled, a root filesystem has to be specified and 818 formatted (for example in the initial ramdisk). 819 ''; 820 }; 821 822 virtualisation.useSecureBoot = 823 mkOption { 824 type = types.bool; 825 default = false; 826 description = 827 lib.mdDoc '' 828 Enable Secure Boot support in the EFI firmware. 829 ''; 830 }; 831 832 833 virtualisation.bios = 834 mkOption { 835 type = types.nullOr types.package; 836 default = null; 837 description = 838 lib.mdDoc '' 839 An alternate BIOS (such as `qboot`) with which to start the VM. 840 Should contain a file named `bios.bin`. 841 If `null`, QEMU's builtin SeaBIOS will be used. 842 ''; 843 }; 844 845 }; 846 847 config = { 848 849 assertions = 850 lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule: 851 [ 852 { assertion = rule.from == "guest" -> rule.proto == "tcp"; 853 message = 854 '' 855 Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto: 856 Guest forwarding supports only TCP connections. 857 ''; 858 } 859 { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address; 860 message = 861 '' 862 Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address: 863 The address must be in the default VLAN (10.0.2.0/24). 864 ''; 865 } 866 ])); 867 868 warnings = 869 optional ( 870 cfg.writableStore && 871 cfg.useNixStoreImage && 872 opt.writableStore.highestPrio > lib.modules.defaultOverridePriority) 873 '' 874 You have enabled ${opt.useNixStoreImage} = true, 875 without setting ${opt.writableStore} = false. 876 877 This causes a store image to be written to the store, which is 878 costly, especially for the binary cache, and because of the need 879 for more frequent garbage collection. 880 881 If you really need this combination, you can set ${opt.writableStore} 882 explicitly to true, incur the cost and make this warning go away. 883 Otherwise, we recommend 884 885 ${opt.writableStore} = false; 886 ''; 887 888 # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install 889 # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs. 890 # Otherwise, we set the proper bootloader device for this. 891 # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint? 892 boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice); 893 boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}"; 894 virtualisation.rootDevice = mkDefault suggestedRootDevice; 895 896 boot.initrd.kernelModules = optionals (cfg.useNixStoreImage && !cfg.writableStore) [ "erofs" ]; 897 898 boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false); 899 900 boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable) 901 '' 902 # We need mke2fs in the initrd. 903 copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs 904 ''; 905 906 boot.initrd.postDeviceCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable) 907 '' 908 # If the disk image appears to be empty, run mke2fs to 909 # initialise. 910 FSTYPE=$(blkid -o value -s TYPE ${cfg.rootDevice} || true) 911 PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.rootDevice} || true) 912 if test -z "$FSTYPE" -a -z "$PARTTYPE"; then 913 mke2fs -t ext4 ${cfg.rootDevice} 914 fi 915 ''; 916 917 boot.initrd.postMountCommands = lib.mkIf (!config.boot.initrd.systemd.enable) 918 '' 919 # Mark this as a NixOS machine. 920 mkdir -p $targetRoot/etc 921 echo -n > $targetRoot/etc/NIXOS 922 923 # Fix the permissions on /tmp. 924 chmod 1777 $targetRoot/tmp 925 926 mkdir -p $targetRoot/boot 927 928 ${optionalString cfg.writableStore '' 929 echo "mounting overlay filesystem on /nix/store..." 930 mkdir -p -m 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store 931 mount -t overlay overlay $targetRoot/nix/store \ 932 -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail 933 ''} 934 ''; 935 936 systemd.tmpfiles.rules = lib.mkIf config.boot.initrd.systemd.enable [ 937 "f /etc/NIXOS 0644 root root -" 938 "d /boot 0644 root root -" 939 ]; 940 941 # After booting, register the closure of the paths in 942 # `virtualisation.additionalPaths' in the Nix database in the VM. This 943 # allows Nix operations to work in the VM. The path to the 944 # registration file is passed through the kernel command line to 945 # allow `system.build.toplevel' to be included. (If we had a direct 946 # reference to ${regInfo} here, then we would get a cyclic 947 # dependency.) 948 boot.postBootCommands = lib.mkIf config.nix.enable 949 '' 950 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then 951 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} 952 fi 953 ''; 954 955 boot.initrd.availableKernelModules = 956 optional cfg.writableStore "overlay" 957 ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"; 958 959 virtualisation.additionalPaths = [ config.system.build.toplevel ]; 960 961 virtualisation.sharedDirectories = { 962 nix-store = mkIf cfg.mountHostNixStore { 963 source = builtins.storeDir; 964 target = "/nix/store"; 965 }; 966 xchg = { 967 source = ''"$TMPDIR"/xchg''; 968 target = "/tmp/xchg"; 969 }; 970 shared = { 971 source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"''; 972 target = "/tmp/shared"; 973 }; 974 }; 975 976 virtualisation.qemu.networkingOptions = 977 let 978 forwardingOptions = flip concatMapStrings cfg.forwardPorts 979 ({ proto, from, host, guest }: 980 if from == "host" 981 then "hostfwd=${proto}:${host.address}:${toString host.port}-" + 982 "${guest.address}:${toString guest.port}," 983 else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" + 984 "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}'," 985 ); 986 restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,"; 987 in 988 [ 989 "-net nic,netdev=user.0,model=virtio" 990 "-netdev user,id=user.0,${forwardingOptions}${restrictNetworkOption}\"$QEMU_NET_OPTS\"" 991 ]; 992 993 # FIXME: Consolidate this one day. 994 virtualisation.qemu.options = mkMerge [ 995 (mkIf cfg.qemu.virtioKeyboard [ 996 "-device virtio-keyboard" 997 ]) 998 (mkIf pkgs.stdenv.hostPlatform.isx86 [ 999 "-usb" "-device usb-tablet,bus=usb-bus.0" 1000 ]) 1001 (mkIf pkgs.stdenv.hostPlatform.isAarch [ 1002 "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet" 1003 ]) 1004 (let 1005 alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9)); 1006 # Replace all non-alphanumeric characters with underscores 1007 sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s); 1008 in mkIf (!cfg.useBootLoader) [ 1009 "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}" 1010 "-initrd ${config.system.build.toplevel}/initrd" 1011 ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' 1012 ]) 1013 (mkIf cfg.useEFIBoot [ 1014 "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}" 1015 "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS" 1016 ]) 1017 (mkIf (cfg.bios != null) [ 1018 "-bios ${cfg.bios}/bios.bin" 1019 ]) 1020 (mkIf (!cfg.graphics) [ 1021 "-nographic" 1022 ]) 1023 ]; 1024 1025 virtualisation.qemu.drives = mkMerge [ 1026 (mkIf (cfg.diskImage != null) [{ 1027 name = "root"; 1028 file = ''"$NIX_DISK_IMAGE"''; 1029 driveExtraOpts.cache = "writeback"; 1030 driveExtraOpts.werror = "report"; 1031 deviceExtraOpts.bootindex = "1"; 1032 }]) 1033 (mkIf cfg.useNixStoreImage [{ 1034 name = "nix-store"; 1035 file = ''"$TMPDIR"/store.img''; 1036 deviceExtraOpts.bootindex = "2"; 1037 driveExtraOpts.format = if cfg.writableStore then "qcow2" else "raw"; 1038 }]) 1039 (imap0 (idx: _: { 1040 file = "$(pwd)/empty${toString idx}.qcow2"; 1041 driveExtraOpts.werror = "report"; 1042 }) cfg.emptyDiskImages) 1043 ]; 1044 1045 fileSystems = mkVMOverride cfg.fileSystems; 1046 1047 # Mount the host filesystem via 9P, and bind-mount the Nix store 1048 # of the host into our own filesystem. We use mkVMOverride to 1049 # allow this module to be applied to "normal" NixOS system 1050 # configuration, where the regular value for the `fileSystems' 1051 # attribute should be disregarded for the purpose of building a VM 1052 # test image (since those filesystems don't exist in the VM). 1053 virtualisation.fileSystems = let 1054 mkSharedDir = tag: share: 1055 { 1056 name = 1057 if tag == "nix-store" && cfg.writableStore 1058 then "/nix/.ro-store" 1059 else share.target; 1060 value.device = tag; 1061 value.fsType = "9p"; 1062 value.neededForBoot = true; 1063 value.options = 1064 [ "trans=virtio" "version=9p2000.L" "msize=${toString cfg.msize}" ] 1065 ++ lib.optional (tag == "nix-store") "cache=loose"; 1066 }; 1067 in lib.mkMerge [ 1068 (lib.mapAttrs' mkSharedDir cfg.sharedDirectories) 1069 { 1070 "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then { 1071 device = "tmpfs"; 1072 fsType = "tmpfs"; 1073 } else { 1074 device = cfg.rootDevice; 1075 fsType = "ext4"; 1076 autoFormat = true; 1077 }); 1078 "/tmp" = lib.mkIf config.boot.tmp.useTmpfs { 1079 device = "tmpfs"; 1080 fsType = "tmpfs"; 1081 neededForBoot = true; 1082 # Sync with systemd's tmp.mount; 1083 options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ]; 1084 }; 1085 "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = lib.mkIf cfg.useNixStoreImage { 1086 device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}"; 1087 neededForBoot = true; 1088 options = [ "ro" ]; 1089 }; 1090 "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) { 1091 fsType = "tmpfs"; 1092 options = [ "mode=0755" ]; 1093 neededForBoot = true; 1094 }; 1095 "/boot" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) { 1096 device = cfg.bootPartition; # 1 for e.g. `vda1`, as created in `systemImage` 1097 fsType = "vfat"; 1098 noCheck = true; # fsck fails on a r/o filesystem 1099 }; 1100 } 1101 ]; 1102 1103 boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) { 1104 mounts = [{ 1105 where = "/sysroot/nix/store"; 1106 what = "overlay"; 1107 type = "overlay"; 1108 options = "lowerdir=/sysroot/nix/.ro-store,upperdir=/sysroot/nix/.rw-store/store,workdir=/sysroot/nix/.rw-store/work"; 1109 wantedBy = ["initrd-fs.target"]; 1110 before = ["initrd-fs.target"]; 1111 requires = ["rw-store.service"]; 1112 after = ["rw-store.service"]; 1113 unitConfig.RequiresMountsFor = "/sysroot/nix/.ro-store"; 1114 }]; 1115 services.rw-store = { 1116 unitConfig = { 1117 DefaultDependencies = false; 1118 RequiresMountsFor = "/sysroot/nix/.rw-store"; 1119 }; 1120 serviceConfig = { 1121 Type = "oneshot"; 1122 ExecStart = "/bin/mkdir -p -m 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store"; 1123 }; 1124 }; 1125 }; 1126 1127 swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ]; 1128 boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) {}; 1129 1130 # Don't run ntpd in the guest. It should get the correct time from KVM. 1131 services.timesyncd.enable = false; 1132 1133 services.qemuGuest.enable = cfg.qemu.guestAgent.enable; 1134 1135 system.build.vm = cfg.host.pkgs.runCommand "nixos-vm" { 1136 preferLocalBuild = true; 1137 meta.mainProgram = "run-${config.system.name}-vm"; 1138 } 1139 '' 1140 mkdir -p $out/bin 1141 ln -s ${config.system.build.toplevel} $out/system 1142 ln -s ${cfg.host.pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm 1143 ''; 1144 1145 # When building a regular system configuration, override whatever 1146 # video driver the host uses. 1147 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ]; 1148 services.xserver.defaultDepth = mkVMOverride 0; 1149 services.xserver.resolutions = mkVMOverride [ cfg.resolution ]; 1150 services.xserver.monitorSection = 1151 '' 1152 # Set a higher refresh rate so that resolutions > 800x600 work. 1153 HorizSync 30-140 1154 VertRefresh 50-160 1155 ''; 1156 1157 # Wireless won't work in the VM. 1158 networking.wireless.enable = mkVMOverride false; 1159 services.connman.enable = mkVMOverride false; 1160 1161 # Speed up booting by not waiting for ARP. 1162 networking.dhcpcd.extraConfig = "noarp"; 1163 1164 networking.usePredictableInterfaceNames = false; 1165 1166 system.requiredKernelConfig = with config.lib.kernelConfig; 1167 [ (isEnabled "VIRTIO_BLK") 1168 (isEnabled "VIRTIO_PCI") 1169 (isEnabled "VIRTIO_NET") 1170 (isEnabled "EXT4_FS") 1171 (isEnabled "NET_9P_VIRTIO") 1172 (isEnabled "9P_FS") 1173 (isYes "BLK_DEV") 1174 (isYes "PCI") 1175 (isYes "NETDEVICES") 1176 (isYes "NET_CORE") 1177 (isYes "INET") 1178 (isYes "NETWORK_FILESYSTEMS") 1179 ] ++ optionals (!cfg.graphics) [ 1180 (isYes "SERIAL_8250_CONSOLE") 1181 (isYes "SERIAL_8250") 1182 ] ++ optionals (cfg.writableStore) [ 1183 (isEnabled "OVERLAY_FS") 1184 ]; 1185 1186 }; 1187 1188 # uses types of services/x11/xserver.nix 1189 meta.buildDocsInSandbox = false; 1190}