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'. By default, the Nix store is shared read-only with the
5 # host, which makes (re)building VMs very efficient.
7 { config, lib, pkgs, options, ... }:
13 qemu-common = import ../../lib/qemu-common.nix { inherit lib pkgs; };
15 cfg = config.virtualisation;
17 opt = options.virtualisation;
19 qemu = cfg.qemu.package;
21 hostPkgs = cfg.host.pkgs;
23 consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
25 driveOpts = { ... }: {
31 description = "The file image used for this drive.";
34 driveExtraOpts = mkOption {
35 type = types.attrsOf types.str;
37 description = "Extra options passed to drive flag.";
40 deviceExtraOpts = mkOption {
41 type = types.attrsOf types.str;
43 description = "Extra options passed to device flag.";
47 type = types.nullOr types.str;
49 description = "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
56 selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }:
57 if useDefaultFilesystems then
58 if useEFIBoot then "efi" else "legacy"
61 driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
63 drvId = "drive${toString idx}";
64 mkKeyValue = generators.mkKeyValueDefault {} "=";
65 mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts);
66 driveOpts = mkOpts (driveExtraOpts // {
72 deviceOpts = mkOpts (deviceExtraOpts // {
76 if cfg.qemu.diskInterface == "scsi" then
77 "-device lsi53c895a -device scsi-hd,${deviceOpts}"
79 "-device virtio-blk-pci,${deviceOpts}";
81 "-drive ${driveOpts} ${device}";
83 drivesCmdLine = drives: concatStringsSep "\\\n " (imap1 driveCmdline drives);
85 # Shell script to start the VM.
88 #! ${hostPkgs.runtimeShell}
90 export PATH=${makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH
94 # Create an empty ext4 filesystem image. A filesystem image does not
95 # contain a partition table but just a filesystem.
96 createEmptyFilesystemImage() {
100 ${qemu}/bin/qemu-img create -f raw "$temp" "$size"
101 ${hostPkgs.e2fsprogs}/bin/mkfs.ext4 -L ${rootFilesystemLabel} "$temp"
102 ${qemu}/bin/qemu-img convert -f raw -O qcow2 "$temp" "$name"
106 NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE"
108 if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then
109 echo "Disk image do not exist, creating the virtualisation disk image..."
111 ${if (cfg.useBootLoader && cfg.useDefaultFilesystems) then ''
112 # Create a writable qcow2 image using the systemImage as a backing
115 # CoW prevent size to be attributed to an image.
116 # FIXME: raise this issue to upstream.
117 ${qemu}/bin/qemu-img create \
119 -b ${systemImage}/nixos.qcow2 \
122 '' else if cfg.useDefaultFilesystems then ''
123 createEmptyFilesystemImage "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
125 # Create an empty disk image without a filesystem.
126 ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
129 echo "Virtualisation disk image created."
132 # Create a directory for storing temporary data of the running VM.
133 if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then
134 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
137 ${lib.optionalString (cfg.useNixStoreImage) ''
138 echo "Creating Nix store image..."
140 ${hostPkgs.gnutar}/bin/tar --create \
142 --verbatim-files-from \
143 --transform 'flags=rSh;s|/nix/store/||' \
144 --transform 'flags=rSh;s|~nix~case~hack~[[:digit:]]\+||g' \
145 --files-from ${hostPkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \
146 | ${hostPkgs.erofs-utils}/bin/mkfs.erofs \
150 -L ${nixStoreFilesystemLabel} \
151 -U eb176051-bd15-49b7-9e6b-462e0b467019 \
156 echo "Created Nix store image."
160 # Create a directory for exchanging data with the VM.
161 mkdir -p "$TMPDIR/xchg"
163 ${lib.optionalString cfg.useHostCerts
165 mkdir -p "$TMPDIR/certs"
166 if [ -e "$NIX_SSL_CERT_FILE" ]; then
167 cp -L "$NIX_SSL_CERT_FILE" "$TMPDIR"/certs/ca-certificates.crt
169 echo \$NIX_SSL_CERT_FILE should point to a valid file if virtualisation.useHostCerts is enabled.
173 ${lib.optionalString cfg.useEFIBoot
175 # Expose EFI variables, it's useful even when we are not using a bootloader (!).
176 # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence
177 # no guard against `useBootLoader`. Examples:
178 # - testing PXE boot or other EFI applications
179 # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows
180 NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}")
181 # VM needs writable EFI vars
182 if ! test -e "$NIX_EFI_VARS"; then
183 ${if cfg.efi.keepVariables then
184 # We still need the EFI var from the make-disk-image derivation
185 # because our "switch-to-configuration" process might
186 # write into it and we want to keep this data.
187 ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"''
189 ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"''
191 chmod 0644 "$NIX_EFI_VARS"
195 ${lib.optionalString cfg.tpm.enable ''
196 NIX_SWTPM_DIR=$(readlink -f "''${NIX_SWTPM_DIR:-${config.system.name}-swtpm}")
197 mkdir -p "$NIX_SWTPM_DIR"
198 ${lib.getExe cfg.tpm.package} \
200 --tpmstate dir="$NIX_SWTPM_DIR" \
201 --ctrl type=unixio,path="$NIX_SWTPM_DIR"/socket,terminate \
202 --pid file="$NIX_SWTPM_DIR"/pid --daemon \
204 --log file="$NIX_SWTPM_DIR"/stdout,level=6
206 # Enable `fdflags` builtin in Bash
207 # We will need it to perform surgical modification of the file descriptor
208 # passed in the coprocess to remove `FD_CLOEXEC`, i.e. close the file descriptor
210 # If let alone, it will trigger the coprocess to read EOF when QEMU is `exec`
211 # at the end of this script. To work around that, we will just clear
212 # the `FD_CLOEXEC` bits as a first step.
213 enable -f ${hostPkgs.bash}/lib/bash/fdflags fdflags
214 # leave a dangling subprocess because the swtpm ctrl socket has
215 # "terminate" when the last connection disconnects, it stops swtpm.
216 # When qemu stops, or if the main shell process ends, the coproc will
217 # get signaled by virtue of the pipe between main and coproc ending.
218 # Which in turns triggers a socat connect-disconnect to swtpm which
220 coproc waitingswtpm {
222 echo "" | ${lib.getExe hostPkgs.socat} STDIO UNIX-CONNECT:"$NIX_SWTPM_DIR"/socket
224 # Clear `FD_CLOEXEC` on the coprocess' file descriptor stdin.
225 fdflags -s-cloexec ''${waitingswtpm[1]}
230 ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"}
231 ${flip concatMapStrings cfg.emptyDiskImages (size: ''
232 if ! test -e "empty$idx.qcow2"; then
233 ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M"
239 exec ${qemu-common.qemuBinary qemu} \
240 -name ${config.system.name} \
241 -m ${toString config.virtualisation.memorySize} \
242 -smp ${toString config.virtualisation.cores} \
243 -device virtio-rng-pci \
244 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
245 ${concatStringsSep " \\\n "
247 (tag: share: "-virtfs local,path=${share.source},security_model=${share.securityModel},mount_tag=${tag}")
248 config.virtualisation.sharedDirectories)} \
249 ${drivesCmdLine config.virtualisation.qemu.drives} \
250 ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \
256 regInfo = hostPkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
258 # Use well-defined and persistent filesystem labels to identify block devices.
259 rootFilesystemLabel = "nixos";
260 espFilesystemLabel = "ESP"; # Hard-coded by make-disk-image.nix
261 nixStoreFilesystemLabel = "nix-store";
263 # The root drive is a raw disk which does not necessarily contain a
264 # filesystem or partition table. It thus cannot be identified via the typical
265 # persistent naming schemes (e.g. /dev/disk/by-{label, uuid, partlabel,
266 # partuuid}. Instead, supply a well-defined and persistent serial attribute
267 # via QEMU. Inside the running system, the disk can then be identified via
268 # the /dev/disk/by-id scheme.
269 rootDriveSerialAttr = "root";
271 # System image is akin to a complete NixOS install with
272 # a boot partition and root partition.
273 systemImage = import ../../lib/make-disk-image.nix {
274 inherit pkgs config lib;
275 additionalPaths = [ regInfo ];
277 onlyNixStore = false;
278 label = rootFilesystemLabel;
279 partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; };
280 installBootLoader = cfg.installBootLoader;
281 touchEFIVars = cfg.useEFIBoot;
283 additionalSpace = "0M";
292 ../profiles/qemu-guest.nix
293 (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
294 (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.")
295 (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")
296 (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.`")
301 virtualisation.fileSystems = options.fileSystems;
303 virtualisation.memorySize =
305 type = types.ints.positive;
308 The memory size in megabytes of the virtual machine.
312 virtualisation.msize =
314 type = types.ints.positive;
317 The msize (maximum packet size) option passed to 9p file systems, in
318 bytes. Increasing this should increase performance significantly,
319 at the cost of higher RAM usage.
323 virtualisation.diskSize =
325 type = types.ints.positive;
328 The disk size in megabytes of the virtual machine.
332 virtualisation.diskImage =
334 type = types.nullOr types.str;
335 default = "./${config.system.name}.qcow2";
336 defaultText = literalExpression ''"./''${config.system.name}.qcow2"'';
338 Path to the disk image containing the root filesystem.
339 The image will be created on startup if it does not
342 If null, a tmpfs will be used as the root filesystem and
343 the VM's state will not be persistent.
347 virtualisation.bootLoaderDevice =
350 default = "/dev/disk/by-id/virtio-${rootDriveSerialAttr}";
351 defaultText = literalExpression ''/dev/disk/by-id/virtio-${rootDriveSerialAttr}'';
352 example = "/dev/disk/by-id/virtio-boot-loader-device";
354 The path (inside th VM) to the device to boot from when legacy booting.
358 virtualisation.bootPartition =
360 type = types.nullOr types.path;
361 default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null;
362 defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null'';
363 example = "/dev/disk/by-label/esp";
365 The path (inside the VM) to the device containing the EFI System Partition (ESP).
367 If you are *not* booting from a UEFI firmware, this value is, by
368 default, `null`. The ESP is mounted to `boot.loader.efi.efiSysMountpoint`.
372 virtualisation.rootDevice =
374 type = types.nullOr types.path;
375 default = "/dev/disk/by-label/${rootFilesystemLabel}";
376 defaultText = literalExpression ''/dev/disk/by-label/${rootFilesystemLabel}'';
377 example = "/dev/disk/by-label/nixos";
379 The path (inside the VM) to the device containing the root filesystem.
383 virtualisation.emptyDiskImages =
385 type = types.listOf types.ints.positive;
388 Additional disk images to provide to the VM. The value is
389 a list of size in megabytes of each disk. These disks are
394 virtualisation.graphics =
399 Whether to run QEMU with a graphics window, or in nographic mode.
400 Serial console will be enabled on both settings, but this will
401 change the preferred console.
405 virtualisation.resolution =
407 type = options.services.xserver.resolutions.type.nestedTypes.elemType;
408 default = { x = 1024; y = 768; };
410 The resolution of the virtual machine display.
414 virtualisation.cores =
416 type = types.ints.positive;
419 Specify the number of cores the guest is permitted to use.
420 The number can be higher than the available cores on the
425 virtualisation.sharedDirectories =
429 options.source = mkOption {
431 description = "The path of the directory to share, can be a shell variable";
433 options.target = mkOption {
435 description = "The mount point of the directory inside the virtual machine";
437 options.securityModel = mkOption {
438 type = types.enum [ "passthrough" "mapped-xattr" "mapped-file" "none" ];
439 default = "mapped-xattr";
441 The security model to use for this share:
443 - `passthrough`: files are stored using the same credentials as they are created on the guest (this requires QEMU to run as root)
444 - `mapped-xattr`: some of the file attributes like uid, gid, mode bits and link target are stored as file attributes
445 - `mapped-file`: the attributes are stored in the hidden .virtfs_metadata directory. Directories exported by this security model cannot interact with other unix tools
446 - `none`: same as "passthrough" except the sever won't report failures if it fails to set file attributes like ownership
452 my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; };
455 An attributes set of directories that will be shared with the
456 virtual machine using VirtFS (9P filesystem over VirtIO).
457 The attribute name will be used as the 9P mount tag.
461 virtualisation.additionalPaths =
463 type = types.listOf types.path;
466 A list of paths whose closure should be made available to
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).
475 When {option}`virtualisation.useNixStoreImage` is
476 set, the closure is copied to the Nix store image.
480 virtualisation.forwardPorts = mkOption {
483 options.from = mkOption {
484 type = types.enum [ "host" "guest" ];
487 Controls the direction in which the ports are mapped:
489 - `"host"` means traffic from the host ports
490 is forwarded to the given guest port.
491 - `"guest"` means traffic from the guest ports
492 is forwarded to the given host port.
495 options.proto = mkOption {
496 type = types.enum [ "tcp" "udp" ];
498 description = "The protocol to forward.";
500 options.host.address = mkOption {
503 description = "The IPv4 address of the host.";
505 options.host.port = mkOption {
507 description = "The host port to be mapped.";
509 options.guest.address = mkOption {
512 description = "The IPv4 address on the guest VLAN.";
514 options.guest.port = mkOption {
516 description = "The guest port to be mapped.";
520 example = lib.literalExpression
522 [ # forward local port 2222 -> 22, to ssh into the VM
523 { from = "host"; host.port = 2222; guest.port = 22; }
525 # forward local port 80 -> 10.0.2.10:80 in the VLAN
527 guest.address = "10.0.2.10"; guest.port = 80;
528 host.address = "127.0.0.1"; host.port = 80;
533 When using the SLiRP user networking (default), this option allows to
534 forward ports to/from the host/guest.
537 If the NixOS firewall on the virtual machine is enabled, you also
538 have to open the guest ports to enable the traffic between host and
543 Currently QEMU supports only IPv4 forwarding.
548 virtualisation.restrictNetwork =
554 If this option is enabled, the guest will be isolated, i.e. it will
555 not be able to contact the host and no guest IP packets will be
556 routed over the host to the outside. This option does not affect
557 any explicitly set forwarding rules.
561 virtualisation.vlans =
563 type = types.listOf types.ints.unsigned;
564 default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ];
565 defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]'';
568 Virtual networks to which the VM is connected. Each
569 number «N» in this list causes
570 the VM to have a virtual Ethernet interface attached to a
571 separate virtual network on which it will be assigned IP
573 `192.168.«N».«M»`,
574 where «M» is the index of this VM
579 virtualisation.interfaces = mkOption {
585 Network interfaces to add to the VM.
587 type = with types; attrsOf (submodule {
590 type = types.ints.unsigned;
592 VLAN to which the network interface is connected.
596 assignIP = mkOption {
600 Automatically assign an IP address to the network interface using the same scheme as
601 virtualisation.vlans.
608 virtualisation.writableStore =
611 default = cfg.mountHostNixStore;
612 defaultText = literalExpression "cfg.mountHostNixStore";
614 If enabled, the Nix store in the VM is made writable by
615 layering an overlay filesystem on top of the host's Nix
618 By default, this is enabled if you mount a host Nix store.
622 virtualisation.writableStoreUseTmpfs =
627 Use a tmpfs for the writable store instead of writing to the VM's
632 networking.primaryIPAddress =
637 description = "Primary IP address used in /etc/hosts.";
640 networking.primaryIPv6Address =
645 description = "Primary IPv6 address used in /etc/hosts.";
648 virtualisation.host.pkgs = mkOption {
649 type = options.nixpkgs.pkgs.type;
651 defaultText = literalExpression "pkgs";
652 example = literalExpression ''
653 import pkgs.path { system = "x86_64-darwin"; }
656 Package set to use for the host-specific packages of the VM runner.
657 Changing this to e.g. a Darwin package set allows running NixOS VMs on Darwin.
661 virtualisation.qemu = {
664 type = types.package;
665 default = if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then hostPkgs.qemu_kvm else hostPkgs.qemu;
666 defaultText = literalExpression "if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then config.virtualisation.host.pkgs.qemu_kvm else config.virtualisation.host.pkgs.qemu";
667 example = literalExpression "pkgs.qemu_test";
668 description = "QEMU package to use.";
673 type = types.listOf types.str;
675 example = [ "-vga std" ];
677 Options passed to QEMU.
678 See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list.
682 consoles = mkOption {
683 type = types.listOf types.str;
685 consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ];
686 in if cfg.graphics then consoles else reverseList consoles;
687 example = [ "console=tty1" ];
689 The output console devices to pass to the kernel command line via the
690 `console` parameter, the primary console is the last
693 By default it enables both serial console and
694 `tty0`. The preferred console (last one) is based on
695 the value of {option}`virtualisation.graphics`.
701 type = types.listOf types.str;
704 "-net nic,netdev=user.0,model=virtio"
705 "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
708 Networking-related command-line options that should be passed to qemu.
709 The default is to use userspace networking (SLiRP).
710 See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details.
712 If you override this option, be advised to keep
713 `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example)
714 to keep the default runtime behaviour.
720 type = types.listOf (types.submodule driveOpts);
721 description = "Drives passed to qemu.";
726 type = types.enum [ "virtio" "scsi" "ide" ];
729 description = "The interface used for the virtual hard disks.";
737 Enable the Qemu guest agent.
746 Enable the virtio-keyboard device.
751 virtualisation.useNixStoreImage =
756 Build and use a disk image for the Nix store, instead of
757 accessing the host's one through 9p.
759 For applications which do a lot of reads from the store,
760 this can drastically improve performance, but at the cost of
761 disk space and image build time.
763 The Nix store image is built just-in-time right before the VM is
764 started. Because it does not produce another derivation, the image is
765 not cached between invocations and never lands in the store or binary
768 If you want a full disk image with a partition table and a root
769 filesystem instead of only a store image, enable
770 {option}`virtualisation.useBootLoader` instead.
774 virtualisation.mountHostNixStore =
777 default = !cfg.useNixStoreImage && !cfg.useBootLoader;
778 defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader";
780 Mount the host Nix store as a 9p mount.
784 virtualisation.directBoot = {
788 default = !cfg.useBootLoader;
789 defaultText = "!cfg.useBootLoader";
791 If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader.
792 Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html)
794 This is enabled by default.
795 If you want to test netboot, consider disabling this option.
796 Enable a bootloader with {option}`virtualisation.useBootLoader` if you need.
798 Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU.
799 Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`.
800 They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details
801 For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`.
803 This will not (re-)boot correctly into a system that has switched to a different configuration on disk.
809 default = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
810 defaultText = "\${config.system.build.initialRamdisk}/\${config.system.boot.loader.initrdFile}";
812 In direct boot situations, you may want to influence the initrd to load
813 to use your own customized payload.
815 This is useful if you want to test the netboot image without
816 testing the firmware or the loading part.
821 virtualisation.useBootLoader =
826 Use a boot loader to boot the system.
827 This allows, among other things, testing the boot loader.
829 If disabled, the kernel and initrd are directly booted,
830 forgoing any bootloader.
832 Check the documentation on {option}`virtualisation.directBoot.enable` for details.
836 virtualisation.installBootLoader =
839 default = cfg.useBootLoader && cfg.useDefaultFilesystems;
840 defaultText = "cfg.useBootLoader && cfg.useDefaultFilesystems";
842 Install boot loader to target image.
844 This is best-effort and may break with unconventional partition setups.
845 Use `virtualisation.useDefaultFilesystems` for a known-working configuration.
849 virtualisation.useEFIBoot =
854 If enabled, the virtual machine will provide a EFI boot
856 useEFIBoot is ignored if useBootLoader == false.
860 virtualisation.efi = {
862 type = types.package;
863 default = (pkgs.OVMF.override {
864 secureBoot = cfg.useSecureBoot;
866 defaultText = ''(pkgs.OVMF.override {
867 secureBoot = cfg.useSecureBoot;
869 description = "OVMF firmware package, defaults to OVMF configured with secure boot if needed.";
872 firmware = mkOption {
874 default = cfg.efi.OVMF.firmware;
875 defaultText = literalExpression "cfg.efi.OVMF.firmware";
877 Firmware binary for EFI implementation, defaults to OVMF.
881 variables = mkOption {
883 default = cfg.efi.OVMF.variables;
884 defaultText = literalExpression "cfg.efi.OVMF.variables";
886 Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware.
891 keepVariables = mkOption {
893 default = cfg.useBootLoader;
894 defaultText = literalExpression "cfg.useBootLoader";
895 description = "Whether to keep EFI variable values from the generated system image";
899 virtualisation.tpm = {
900 enable = mkEnableOption "a TPM device in the virtual machine with a driver, using swtpm";
902 package = mkPackageOption cfg.host.pkgs "swtpm" { };
904 deviceModel = mkOption {
907 "i686-linux" = "tpm-tis";
908 "x86_64-linux" = "tpm-tis";
909 "ppc64-linux" = "tpm-spapr";
910 "armv7-linux" = "tpm-tis-device";
911 "aarch64-linux" = "tpm-tis-device";
912 }.${pkgs.stdenv.hostPlatform.system} or (throw "Unsupported system for TPM2 emulation in QEMU"));
914 Based on the guest platform Linux system:
916 - `tpm-tis` for (i686, x86_64)
917 - `tpm-spapr` for ppc64
918 - `tpm-tis-device` for (armv7, aarch64)
920 example = "tpm-tis-device";
921 description = "QEMU device model for the TPM, uses the appropriate default based on th guest platform system and the package passed.";
925 virtualisation.useDefaultFilesystems =
930 If enabled, the boot disk of the virtual machine will be
931 formatted and mounted with the default filesystems for
932 testing. Swap devices and LUKS will be disabled.
934 If disabled, a root filesystem has to be specified and
935 formatted (for example in the initial ramdisk).
939 virtualisation.useSecureBoot =
944 Enable Secure Boot support in the EFI firmware.
948 virtualisation.bios =
950 type = types.nullOr types.package;
953 An alternate BIOS (such as `qboot`) with which to start the VM.
954 Should contain a file named `bios.bin`.
955 If `null`, QEMU's builtin SeaBIOS will be used.
959 virtualisation.useHostCerts =
964 If enabled, when `NIX_SSL_CERT_FILE` is set on the host,
965 pass the CA certificates from the host to the VM.
974 lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule:
976 { assertion = rule.from == "guest" -> rule.proto == "tcp";
979 Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto:
980 Guest forwarding supports only TCP connections.
983 { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address;
986 Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address:
987 The address must be in the default VLAN (10.0.2.0/24).
991 { assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047;
993 virtualisation.memorySize is above 2047, but qemu is only able to allocate 2047MB RAM on 32bit max.
996 { assertion = cfg.directBoot.enable || cfg.directBoot.initrd == options.virtualisation.directBoot.initrd.default;
999 You changed the default of `virtualisation.directBoot.initrd` but you are not
1000 using QEMU direct boot. This initrd will not be used in your current
1003 Either do not mutate `virtualisation.directBoot.initrd` or enable direct boot.
1005 If you have a more advanced usecase, please open an issue or a pull request.
1009 assertion = cfg.installBootLoader -> config.system.switch.enable;
1011 `system.switch.enable` must be enabled for `virtualisation.installBootLoader` to work.
1012 Please enable it in your configuration.
1018 optional (cfg.directBoot.enable && cfg.useBootLoader)
1020 You enabled direct boot and a bootloader, QEMU will not boot your bootloader, rendering
1021 `useBootLoader` useless. You might want to disable one of those options.
1024 # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install
1025 # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs.
1026 # Otherwise, we set the proper bootloader device for this.
1027 # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint?
1028 boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice);
1029 boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
1031 boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false);
1033 # After booting, register the closure of the paths in
1034 # `virtualisation.additionalPaths' in the Nix database in the VM. This
1035 # allows Nix operations to work in the VM. The path to the
1036 # registration file is passed through the kernel command line to
1037 # allow `system.build.toplevel' to be included. (If we had a direct
1038 # reference to ${regInfo} here, then we would get a cyclic
1040 boot.postBootCommands = lib.mkIf config.nix.enable
1042 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
1043 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
1047 boot.initrd.availableKernelModules =
1048 optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"
1049 ++ optional (cfg.tpm.enable) "tpm_tis";
1051 virtualisation.additionalPaths = [ config.system.build.toplevel ];
1053 virtualisation.sharedDirectories = {
1054 nix-store = mkIf cfg.mountHostNixStore {
1055 source = builtins.storeDir;
1056 # Always mount this to /nix/.ro-store because we never want to actually
1057 # write to the host Nix Store.
1058 target = "/nix/.ro-store";
1059 securityModel = "none";
1062 source = ''"$TMPDIR"/xchg'';
1063 securityModel = "none";
1064 target = "/tmp/xchg";
1067 source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
1068 target = "/tmp/shared";
1069 securityModel = "none";
1071 certs = mkIf cfg.useHostCerts {
1072 source = ''"$TMPDIR"/certs'';
1073 target = "/etc/ssl/certs";
1074 securityModel = "none";
1078 security.pki.installCACerts = mkIf cfg.useHostCerts false;
1080 virtualisation.qemu.networkingOptions =
1082 forwardingOptions = flip concatMapStrings cfg.forwardPorts
1083 ({ proto, from, host, guest }:
1085 then "hostfwd=${proto}:${host.address}:${toString host.port}-" +
1086 "${guest.address}:${toString guest.port},"
1087 else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
1088 "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
1090 restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,";
1093 "-net nic,netdev=user.0,model=virtio"
1094 "-netdev user,id=user.0,${forwardingOptions}${restrictNetworkOption}\"$QEMU_NET_OPTS\""
1097 virtualisation.qemu.options = mkMerge [
1098 (mkIf cfg.qemu.virtioKeyboard [
1099 "-device virtio-keyboard"
1101 (mkIf pkgs.stdenv.hostPlatform.isx86 [
1102 "-usb" "-device usb-tablet,bus=usb-bus.0"
1104 (mkIf pkgs.stdenv.hostPlatform.isAarch [
1105 "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet"
1108 alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
1109 # Replace all non-alphanumeric characters with underscores
1110 sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s);
1111 in mkIf cfg.directBoot.enable [
1112 "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}"
1113 "-initrd ${cfg.directBoot.initrd}"
1114 ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
1116 (mkIf cfg.useEFIBoot [
1117 "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
1118 "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS"
1120 (mkIf (cfg.bios != null) [
1121 "-bios ${cfg.bios}/bios.bin"
1123 (mkIf (!cfg.graphics) [
1126 (mkIf (cfg.tpm.enable) [
1127 "-chardev socket,id=chrtpm,path=\"$NIX_SWTPM_DIR\"/socket"
1128 "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm"
1129 "-device ${cfg.tpm.deviceModel},tpmdev=tpm_dev_0"
1131 (mkIf (pkgs.stdenv.hostPlatform.isx86 && cfg.efi.OVMF.systemManagementModeRequired) [
1132 "-machine" "q35,smm=on"
1133 "-global" "driver=cfi.pflash01,property=secure,value=on"
1137 virtualisation.qemu.drives = mkMerge [
1138 (mkIf (cfg.diskImage != null) [{
1140 file = ''"$NIX_DISK_IMAGE"'';
1141 driveExtraOpts.cache = "writeback";
1142 driveExtraOpts.werror = "report";
1143 deviceExtraOpts.bootindex = "1";
1144 deviceExtraOpts.serial = rootDriveSerialAttr;
1146 (mkIf cfg.useNixStoreImage [{
1148 file = ''"$TMPDIR"/store.img'';
1149 deviceExtraOpts.bootindex = "2";
1150 driveExtraOpts.format = "raw";
1153 file = "$(pwd)/empty${toString idx}.qcow2";
1154 driveExtraOpts.werror = "report";
1155 }) cfg.emptyDiskImages)
1158 # By default, use mkVMOverride to enable building test VMs (e.g. via
1159 # `nixos-rebuild build-vm`) of a system configuration, where the regular
1160 # value for the `fileSystems' attribute should be disregarded (since those
1161 # filesystems don't necessarily exist in the VM). You can disable this
1162 # override by setting `virtualisation.fileSystems = lib.mkForce { };`.
1163 fileSystems = lib.mkIf (cfg.fileSystems != { }) (mkVMOverride cfg.fileSystems);
1165 virtualisation.fileSystems = let
1166 mkSharedDir = tag: share:
1168 name = share.target;
1170 value.fsType = "9p";
1171 value.neededForBoot = true;
1173 [ "trans=virtio" "version=9p2000.L" "msize=${toString cfg.msize}" "x-systemd.requires=modprobe@9pnet_virtio.service" ]
1174 ++ lib.optional (tag == "nix-store") "cache=loose";
1177 (lib.mapAttrs' mkSharedDir cfg.sharedDirectories)
1179 "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then {
1183 device = cfg.rootDevice;
1186 "/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
1189 neededForBoot = true;
1190 # Sync with systemd's tmp.mount;
1191 options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ];
1193 "/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) (if cfg.writableStore then {
1195 lowerdir = [ "/nix/.ro-store" ];
1196 upperdir = "/nix/.rw-store/upper";
1197 workdir = "/nix/.rw-store/work";
1200 device = "/nix/.ro-store";
1201 options = [ "bind" ];
1203 "/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage {
1204 device = "/dev/disk/by-label/${nixStoreFilesystemLabel}";
1206 neededForBoot = true;
1209 "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
1211 options = [ "mode=0755" ];
1212 neededForBoot = true;
1214 "${config.boot.loader.efi.efiSysMountPoint}" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) {
1215 device = cfg.bootPartition;
1221 swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ];
1222 boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) {};
1224 # Don't run ntpd in the guest. It should get the correct time from KVM.
1225 services.timesyncd.enable = false;
1227 services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
1229 system.build.vm = hostPkgs.runCommand "nixos-vm" {
1230 preferLocalBuild = true;
1231 meta.mainProgram = "run-${config.system.name}-vm";
1235 ln -s ${config.system.build.toplevel} $out/system
1236 ln -s ${hostPkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
1239 # When building a regular system configuration, override whatever
1240 # video driver the host uses.
1241 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ];
1242 services.xserver.defaultDepth = mkVMOverride 0;
1243 services.xserver.resolutions = mkVMOverride [ cfg.resolution ];
1244 services.xserver.monitorSection =
1246 # Set a higher refresh rate so that resolutions > 800x600 work.
1251 # Wireless won't work in the VM.
1252 networking.wireless.enable = mkVMOverride false;
1253 services.connman.enable = mkVMOverride false;
1255 # Speed up booting by not waiting for ARP.
1256 networking.dhcpcd.extraConfig = "noarp";
1258 networking.usePredictableInterfaceNames = false;
1260 system.requiredKernelConfig = with config.lib.kernelConfig;
1261 [ (isEnabled "VIRTIO_BLK")
1262 (isEnabled "VIRTIO_PCI")
1263 (isEnabled "VIRTIO_NET")
1264 (isEnabled "EXT4_FS")
1265 (isEnabled "NET_9P_VIRTIO")
1269 (isYes "NETDEVICES")
1272 (isYes "NETWORK_FILESYSTEMS")
1273 ] ++ optionals (!cfg.graphics) [
1274 (isYes "SERIAL_8250_CONSOLE")
1275 (isYes "SERIAL_8250")
1276 ] ++ optionals (cfg.writableStore) [
1277 (isEnabled "OVERLAY_FS")
1282 # uses types of services/x11/xserver.nix
1283 meta.buildDocsInSandbox = false;