grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / virtualisation / qemu-vm.nix
blobedefb4c227f033124595edef516140da75927a39
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, ... }:
9 with lib;
11 let
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 = { ... }: {
27     options = {
29       file = mkOption {
30         type = types.str;
31         description = "The file image used for this drive.";
32       };
34       driveExtraOpts = mkOption {
35         type = types.attrsOf types.str;
36         default = {};
37         description = "Extra options passed to drive flag.";
38       };
40       deviceExtraOpts = mkOption {
41         type = types.attrsOf types.str;
42         default = {};
43         description = "Extra options passed to device flag.";
44       };
46       name = mkOption {
47         type = types.nullOr types.str;
48         default = null;
49         description = "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
50       };
52     };
54   };
56   selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }:
57   if useDefaultFilesystems then
58     if useEFIBoot then "efi" else "legacy"
59   else "none";
61   driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
62     let
63       drvId = "drive${toString idx}";
64       mkKeyValue = generators.mkKeyValueDefault {} "=";
65       mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts);
66       driveOpts = mkOpts (driveExtraOpts // {
67         index = idx;
68         id = drvId;
69         "if" = "none";
70         inherit file;
71       });
72       deviceOpts = mkOpts (deviceExtraOpts // {
73         drive = drvId;
74       });
75       device =
76         if cfg.qemu.diskInterface == "scsi" then
77           "-device lsi53c895a -device scsi-hd,${deviceOpts}"
78         else
79           "-device virtio-blk-pci,${deviceOpts}";
80     in
81       "-drive ${driveOpts} ${device}";
83   drivesCmdLine = drives: concatStringsSep "\\\n    " (imap1 driveCmdline drives);
85   # Shell script to start the VM.
86   startVM =
87     ''
88       #! ${hostPkgs.runtimeShell}
90       export PATH=${makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH
92       set -e
94       # Create an empty ext4 filesystem image. A filesystem image does not
95       # contain a partition table but just a filesystem.
96       createEmptyFilesystemImage() {
97         local name=$1
98         local size=$2
99         local temp=$(mktemp)
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"
103         rm "$temp"
104       }
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
113             # image.
115             # CoW prevent size to be attributed to an image.
116             # FIXME: raise this issue to upstream.
117             ${qemu}/bin/qemu-img create \
118               -f qcow2 \
119               -b ${systemImage}/nixos.qcow2 \
120               -F qcow2 \
121               "$NIX_DISK_IMAGE"
122           '' else if cfg.useDefaultFilesystems then ''
123             createEmptyFilesystemImage "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
124           '' else ''
125             # Create an empty disk image without a filesystem.
126             ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M"
127           ''
128           }
129           echo "Virtualisation disk image created."
130       fi
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)
135       fi
137       ${lib.optionalString (cfg.useNixStoreImage) ''
138         echo "Creating Nix store image..."
140         ${hostPkgs.gnutar}/bin/tar --create \
141           --absolute-names \
142           --verbatim-files-from \
143           --transform 'flags=rSh;s|/nix/store/||' \
144           --files-from ${hostPkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \
145           | ${hostPkgs.erofs-utils}/bin/mkfs.erofs \
146             --quiet \
147             --force-uid=0 \
148             --force-gid=0 \
149             -L ${nixStoreFilesystemLabel} \
150             -U eb176051-bd15-49b7-9e6b-462e0b467019 \
151             -T 0 \
152             --tar=f \
153             "$TMPDIR"/store.img
155         echo "Created Nix store image."
156       ''
157       }
159       # Create a directory for exchanging data with the VM.
160       mkdir -p "$TMPDIR/xchg"
162       ${lib.optionalString cfg.useHostCerts
163       ''
164         mkdir -p "$TMPDIR/certs"
165         if [ -e "$NIX_SSL_CERT_FILE" ]; then
166           cp -L "$NIX_SSL_CERT_FILE" "$TMPDIR"/certs/ca-certificates.crt
167         else
168           echo \$NIX_SSL_CERT_FILE should point to a valid file if virtualisation.useHostCerts is enabled.
169         fi
170       ''}
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.efi.keepVariables 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       ''}
194       ${lib.optionalString cfg.tpm.enable ''
195         NIX_SWTPM_DIR=$(readlink -f "''${NIX_SWTPM_DIR:-${config.system.name}-swtpm}")
196         mkdir -p "$NIX_SWTPM_DIR"
197         ${lib.getExe cfg.tpm.package} \
198           socket \
199           --tpmstate dir="$NIX_SWTPM_DIR" \
200           --ctrl type=unixio,path="$NIX_SWTPM_DIR"/socket,terminate \
201           --pid file="$NIX_SWTPM_DIR"/pid --daemon \
202           --tpm2 \
203           --log file="$NIX_SWTPM_DIR"/stdout,level=6
205         # Enable `fdflags` builtin in Bash
206         # We will need it to perform surgical modification of the file descriptor
207         # passed in the coprocess to remove `FD_CLOEXEC`, i.e. close the file descriptor
208         # on exec.
209         # If let alone, it will trigger the coprocess to read EOF when QEMU is `exec`
210         # at the end of this script. To work around that, we will just clear
211         # the `FD_CLOEXEC` bits as a first step.
212         enable -f ${hostPkgs.bash}/lib/bash/fdflags fdflags
213         # leave a dangling subprocess because the swtpm ctrl socket has
214         # "terminate" when the last connection disconnects, it stops swtpm.
215         # When qemu stops, or if the main shell process ends, the coproc will
216         # get signaled by virtue of the pipe between main and coproc ending.
217         # Which in turns triggers a socat connect-disconnect to swtpm which
218         # will stop it.
219         coproc waitingswtpm {
220           read || :
221           echo "" | ${lib.getExe hostPkgs.socat} STDIO UNIX-CONNECT:"$NIX_SWTPM_DIR"/socket
222         }
223         # Clear `FD_CLOEXEC` on the coprocess' file descriptor stdin.
224         fdflags -s-cloexec ''${waitingswtpm[1]}
225       ''}
227       cd "$TMPDIR"
229       ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"}
230       ${flip concatMapStrings cfg.emptyDiskImages (size: ''
231         if ! test -e "empty$idx.qcow2"; then
232             ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M"
233         fi
234         idx=$((idx + 1))
235       '')}
237       # Start QEMU.
238       exec ${qemu-common.qemuBinary qemu} \
239           -name ${config.system.name} \
240           -m ${toString config.virtualisation.memorySize} \
241           -smp ${toString config.virtualisation.cores} \
242           -device virtio-rng-pci \
243           ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
244           ${concatStringsSep " \\\n    "
245             (mapAttrsToList
246               (tag: share: "-virtfs local,path=${share.source},security_model=${share.securityModel},mount_tag=${tag}")
247               config.virtualisation.sharedDirectories)} \
248           ${drivesCmdLine config.virtualisation.qemu.drives} \
249           ${concatStringsSep " \\\n    " config.virtualisation.qemu.options} \
250           $QEMU_OPTS \
251           "$@"
252     '';
255   regInfo = hostPkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
257   # Use well-defined and persistent filesystem labels to identify block devices.
258   rootFilesystemLabel = "nixos";
259   espFilesystemLabel = "ESP"; # Hard-coded by make-disk-image.nix
260   nixStoreFilesystemLabel = "nix-store";
262   # The root drive is a raw disk which does not necessarily contain a
263   # filesystem or partition table. It thus cannot be identified via the typical
264   # persistent naming schemes (e.g. /dev/disk/by-{label, uuid, partlabel,
265   # partuuid}. Instead, supply a well-defined and persistent serial attribute
266   # via QEMU. Inside the running system, the disk can then be identified via
267   # the /dev/disk/by-id scheme.
268   rootDriveSerialAttr = "root";
270   # System image is akin to a complete NixOS install with
271   # a boot partition and root partition.
272   systemImage = import ../../lib/make-disk-image.nix {
273     inherit pkgs config lib;
274     additionalPaths = [ regInfo ];
275     format = "qcow2";
276     onlyNixStore = false;
277     label = rootFilesystemLabel;
278     partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; };
279     installBootLoader = cfg.installBootLoader;
280     touchEFIVars = cfg.useEFIBoot;
281     diskSize = "auto";
282     additionalSpace = "0M";
283     copyChannel = false;
284     OVMF = cfg.efi.OVMF;
285   };
290   imports = [
291     ../profiles/qemu-guest.nix
292     (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
293     (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.")
294     (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")
295     (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.`")
296   ];
298   options = {
300     virtualisation.fileSystems = options.fileSystems;
302     virtualisation.memorySize =
303       mkOption {
304         type = types.ints.positive;
305         default = 1024;
306         description = ''
307             The memory size in megabytes of the virtual machine.
308           '';
309       };
311     virtualisation.msize =
312       mkOption {
313         type = types.ints.positive;
314         default = 16384;
315         description = ''
316             The msize (maximum packet size) option passed to 9p file systems, in
317             bytes. Increasing this should increase performance significantly,
318             at the cost of higher RAM usage.
319           '';
320       };
322     virtualisation.diskSize =
323       mkOption {
324         type = types.ints.positive;
325         default = 1024;
326         description = ''
327             The disk size in megabytes of the virtual machine.
328           '';
329       };
331     virtualisation.diskImage =
332       mkOption {
333         type = types.nullOr types.str;
334         default = "./${config.system.name}.qcow2";
335         defaultText = literalExpression ''"./''${config.system.name}.qcow2"'';
336         description = ''
337             Path to the disk image containing the root filesystem.
338             The image will be created on startup if it does not
339             exist.
341             If null, a tmpfs will be used as the root filesystem and
342             the VM's state will not be persistent.
343           '';
344       };
346     virtualisation.bootLoaderDevice =
347       mkOption {
348         type = types.path;
349         default = "/dev/disk/by-id/virtio-${rootDriveSerialAttr}";
350         defaultText = literalExpression ''/dev/disk/by-id/virtio-${rootDriveSerialAttr}'';
351         example = "/dev/disk/by-id/virtio-boot-loader-device";
352         description = ''
353             The path (inside th VM) to the device to boot from when legacy booting.
354           '';
355         };
357     virtualisation.bootPartition =
358       mkOption {
359         type = types.nullOr types.path;
360         default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null;
361         defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null'';
362         example = "/dev/disk/by-label/esp";
363         description = ''
364             The path (inside the VM) to the device containing the EFI System Partition (ESP).
366             If you are *not* booting from a UEFI firmware, this value is, by
367             default, `null`. The ESP is mounted to `boot.loader.efi.efiSysMountpoint`.
368           '';
369       };
371     virtualisation.rootDevice =
372       mkOption {
373         type = types.nullOr types.path;
374         default = "/dev/disk/by-label/${rootFilesystemLabel}";
375         defaultText = literalExpression ''/dev/disk/by-label/${rootFilesystemLabel}'';
376         example = "/dev/disk/by-label/nixos";
377         description = ''
378             The path (inside the VM) to the device containing the root filesystem.
379           '';
380       };
382     virtualisation.emptyDiskImages =
383       mkOption {
384         type = types.listOf types.ints.positive;
385         default = [];
386         description = ''
387             Additional disk images to provide to the VM. The value is
388             a list of size in megabytes of each disk. These disks are
389             writeable by the VM.
390           '';
391       };
393     virtualisation.graphics =
394       mkOption {
395         type = types.bool;
396         default = true;
397         description = ''
398             Whether to run QEMU with a graphics window, or in nographic mode.
399             Serial console will be enabled on both settings, but this will
400             change the preferred console.
401             '';
402       };
404     virtualisation.resolution =
405       mkOption {
406         type = options.services.xserver.resolutions.type.nestedTypes.elemType;
407         default = { x = 1024; y = 768; };
408         description = ''
409             The resolution of the virtual machine display.
410           '';
411       };
413     virtualisation.cores =
414       mkOption {
415         type = types.ints.positive;
416         default = 1;
417         description = ''
418             Specify the number of cores the guest is permitted to use.
419             The number can be higher than the available cores on the
420             host system.
421           '';
422       };
424     virtualisation.sharedDirectories =
425       mkOption {
426         type = types.attrsOf
427           (types.submodule {
428             options.source = mkOption {
429               type = types.str;
430               description = "The path of the directory to share, can be a shell variable";
431             };
432             options.target = mkOption {
433               type = types.path;
434               description = "The mount point of the directory inside the virtual machine";
435             };
436             options.securityModel = mkOption {
437               type = types.enum [ "passthrough" "mapped-xattr" "mapped-file" "none" ];
438               default = "mapped-xattr";
439               description = ''
440                 The security model to use for this share:
442                 - `passthrough`: files are stored using the same credentials as they are created on the guest (this requires QEMU to run as root)
443                 - `mapped-xattr`: some of the file attributes like uid, gid, mode bits and link target are stored as file attributes
444                 - `mapped-file`: the attributes are stored in the hidden .virtfs_metadata directory. Directories exported by this security model cannot interact with other unix tools
445                 - `none`: same as "passthrough" except the sever won't report failures if it fails to set file attributes like ownership
446               '';
447             };
448           });
449         default = { };
450         example = {
451           my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; };
452         };
453         description = ''
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       };
460     virtualisation.additionalPaths =
461       mkOption {
462         type = types.listOf types.path;
463         default = [];
464         description = ''
465             A list of paths whose closure should be made available to
466             the VM.
468             When 9p is used, the closure is registered in the Nix
469             database in the VM. All other paths in the host Nix store
470             appear in the guest Nix store as well, but are considered
471             garbage (because they are not registered in the Nix
472             database of the guest).
474             When {option}`virtualisation.useNixStoreImage` is
475             set, the closure is copied to the Nix store image.
476           '';
477       };
479     virtualisation.forwardPorts = mkOption {
480       type = types.listOf
481         (types.submodule {
482           options.from = mkOption {
483             type = types.enum [ "host" "guest" ];
484             default = "host";
485             description = ''
486                 Controls the direction in which the ports are mapped:
488                 - `"host"` means traffic from the host ports
489                   is forwarded to the given guest port.
490                 - `"guest"` means traffic from the guest ports
491                   is forwarded to the given host port.
492               '';
493           };
494           options.proto = mkOption {
495             type = types.enum [ "tcp" "udp" ];
496             default = "tcp";
497             description = "The protocol to forward.";
498           };
499           options.host.address = mkOption {
500             type = types.str;
501             default = "";
502             description = "The IPv4 address of the host.";
503           };
504           options.host.port = mkOption {
505             type = types.port;
506             description = "The host port to be mapped.";
507           };
508           options.guest.address = mkOption {
509             type = types.str;
510             default = "";
511             description = "The IPv4 address on the guest VLAN.";
512           };
513           options.guest.port = mkOption {
514             type = types.port;
515             description = "The guest port to be mapped.";
516           };
517         });
518       default = [];
519       example = lib.literalExpression
520         ''
521         [ # forward local port 2222 -> 22, to ssh into the VM
522           { from = "host"; host.port = 2222; guest.port = 22; }
524           # forward local port 80 -> 10.0.2.10:80 in the VLAN
525           { from = "guest";
526             guest.address = "10.0.2.10"; guest.port = 80;
527             host.address = "127.0.0.1"; host.port = 80;
528           }
529         ]
530         '';
531       description = ''
532           When using the SLiRP user networking (default), this option allows to
533           forward ports to/from the host/guest.
535           ::: {.warning}
536           If the NixOS firewall on the virtual machine is enabled, you also
537           have to open the guest ports to enable the traffic between host and
538           guest.
539           :::
541           ::: {.note}
542           Currently QEMU supports only IPv4 forwarding.
543           :::
544         '';
545     };
547     virtualisation.restrictNetwork =
548       mkOption {
549         type = types.bool;
550         default = false;
551         example = true;
552         description = ''
553             If this option is enabled, the guest will be isolated, i.e. it will
554             not be able to contact the host and no guest IP packets will be
555             routed over the host to the outside. This option does not affect
556             any explicitly set forwarding rules.
557           '';
558       };
560     virtualisation.vlans =
561       mkOption {
562         type = types.listOf types.ints.unsigned;
563         default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ];
564         defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]'';
565         example = [ 1 2 ];
566         description = ''
567             Virtual networks to which the VM is connected.  Each
568             number Â«N» in this list causes
569             the VM to have a virtual Ethernet interface attached to a
570             separate virtual network on which it will be assigned IP
571             address
572             `192.168.«N».«M»`,
573             where Â«M» is the index of this VM
574             in the list of VMs.
575           '';
576       };
578     virtualisation.interfaces = mkOption {
579       default = {};
580       example = {
581         enp1s0.vlan = 1;
582       };
583       description = ''
584         Network interfaces to add to the VM.
585       '';
586       type = with types; attrsOf (submodule {
587         options = {
588           vlan = mkOption {
589             type = types.ints.unsigned;
590             description = ''
591               VLAN to which the network interface is connected.
592             '';
593           };
595           assignIP = mkOption {
596             type = types.bool;
597             default = false;
598             description = ''
599               Automatically assign an IP address to the network interface using the same scheme as
600               virtualisation.vlans.
601             '';
602           };
603         };
604       });
605     };
607     virtualisation.writableStore =
608       mkOption {
609         type = types.bool;
610         default = cfg.mountHostNixStore;
611         defaultText = literalExpression "cfg.mountHostNixStore";
612         description = ''
613             If enabled, the Nix store in the VM is made writable by
614             layering an overlay filesystem on top of the host's Nix
615             store.
617             By default, this is enabled if you mount a host Nix store.
618           '';
619       };
621     virtualisation.writableStoreUseTmpfs =
622       mkOption {
623         type = types.bool;
624         default = true;
625         description = ''
626             Use a tmpfs for the writable store instead of writing to the VM's
627             own filesystem.
628           '';
629       };
631     networking.primaryIPAddress =
632       mkOption {
633         type = types.str;
634         default = "";
635         internal = true;
636         description = "Primary IP address used in /etc/hosts.";
637       };
639     networking.primaryIPv6Address =
640       mkOption {
641         type = types.str;
642         default = "";
643         internal = true;
644         description = "Primary IPv6 address used in /etc/hosts.";
645       };
647     virtualisation.host.pkgs = mkOption {
648       type = options.nixpkgs.pkgs.type;
649       default = pkgs;
650       defaultText = literalExpression "pkgs";
651       example = literalExpression ''
652         import pkgs.path { system = "x86_64-darwin"; }
653       '';
654       description = ''
655         Package set to use for the host-specific packages of the VM runner.
656         Changing this to e.g. a Darwin package set allows running NixOS VMs on Darwin.
657       '';
658     };
660     virtualisation.qemu = {
661       package =
662         mkOption {
663           type = types.package;
664           default = if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then hostPkgs.qemu_kvm else hostPkgs.qemu;
665           defaultText = literalExpression "if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then config.virtualisation.host.pkgs.qemu_kvm else config.virtualisation.host.pkgs.qemu";
666           example = literalExpression "pkgs.qemu_test";
667           description = "QEMU package to use.";
668         };
670       options =
671         mkOption {
672           type = types.listOf types.str;
673           default = [];
674           example = [ "-vga std" ];
675           description = ''
676             Options passed to QEMU.
677             See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list.
678           '';
679         };
681       consoles = mkOption {
682         type = types.listOf types.str;
683         default = let
684           consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ];
685         in if cfg.graphics then consoles else reverseList consoles;
686         example = [ "console=tty1" ];
687         description = ''
688           The output console devices to pass to the kernel command line via the
689           `console` parameter, the primary console is the last
690           item of this list.
692           By default it enables both serial console and
693           `tty0`. The preferred console (last one) is based on
694           the value of {option}`virtualisation.graphics`.
695         '';
696       };
698       networkingOptions =
699         mkOption {
700           type = types.listOf types.str;
701           default = [ ];
702           example = [
703             "-net nic,netdev=user.0,model=virtio"
704             "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
705           ];
706           description = ''
707             Networking-related command-line options that should be passed to qemu.
708             The default is to use userspace networking (SLiRP).
709             See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details.
711             If you override this option, be advised to keep
712             `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example)
713             to keep the default runtime behaviour.
714           '';
715         };
717       drives =
718         mkOption {
719           type = types.listOf (types.submodule driveOpts);
720           description = "Drives passed to qemu.";
721         };
723       diskInterface =
724         mkOption {
725           type = types.enum [ "virtio" "scsi" "ide" ];
726           default = "virtio";
727           example = "scsi";
728           description = "The interface used for the virtual hard disks.";
729         };
731       guestAgent.enable =
732         mkOption {
733           type = types.bool;
734           default = true;
735           description = ''
736             Enable the Qemu guest agent.
737           '';
738         };
740       virtioKeyboard =
741         mkOption {
742           type = types.bool;
743           default = true;
744           description = ''
745             Enable the virtio-keyboard device.
746           '';
747         };
748     };
750     virtualisation.useNixStoreImage =
751       mkOption {
752         type = types.bool;
753         default = false;
754         description = ''
755           Build and use a disk image for the Nix store, instead of
756           accessing the host's one through 9p.
758           For applications which do a lot of reads from the store,
759           this can drastically improve performance, but at the cost of
760           disk space and image build time.
762           The Nix store image is built just-in-time right before the VM is
763           started. Because it does not produce another derivation, the image is
764           not cached between invocations and never lands in the store or binary
765           cache.
767           If you want a full disk image with a partition table and a root
768           filesystem instead of only a store image, enable
769           {option}`virtualisation.useBootLoader` instead.
770         '';
771       };
773     virtualisation.mountHostNixStore =
774       mkOption {
775         type = types.bool;
776         default = !cfg.useNixStoreImage && !cfg.useBootLoader;
777         defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader";
778         description = ''
779           Mount the host Nix store as a 9p mount.
780         '';
781       };
783     virtualisation.directBoot = {
784       enable =
785         mkOption {
786           type = types.bool;
787           default = !cfg.useBootLoader;
788           defaultText = "!cfg.useBootLoader";
789           description = ''
790               If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader.
791               Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html)
793               This is enabled by default.
794               If you want to test netboot, consider disabling this option.
795               Enable a bootloader with {option}`virtualisation.useBootLoader` if you need.
797               Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU.
798               Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`.
799               They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details
800               For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`.
802               This will not (re-)boot correctly into a system that has switched to a different configuration on disk.
803             '';
804         };
805       initrd =
806         mkOption {
807           type = types.str;
808           default = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
809           defaultText = "\${config.system.build.initialRamdisk}/\${config.system.boot.loader.initrdFile}";
810           description = ''
811               In direct boot situations, you may want to influence the initrd to load
812               to use your own customized payload.
814               This is useful if you want to test the netboot image without
815               testing the firmware or the loading part.
816             '';
817         };
818     };
820     virtualisation.useBootLoader =
821       mkOption {
822         type = types.bool;
823         default = false;
824         description = ''
825             Use a boot loader to boot the system.
826             This allows, among other things, testing the boot loader.
828             If disabled, the kernel and initrd are directly booted,
829             forgoing any bootloader.
831             Check the documentation on {option}`virtualisation.directBoot.enable` for details.
832           '';
833       };
835     virtualisation.installBootLoader =
836       mkOption {
837         type = types.bool;
838         default = cfg.useBootLoader && cfg.useDefaultFilesystems;
839         defaultText = "cfg.useBootLoader && cfg.useDefaultFilesystems";
840         description = ''
841           Install boot loader to target image.
843           This is best-effort and may break with unconventional partition setups.
844           Use `virtualisation.useDefaultFilesystems` for a known-working configuration.
845         '';
846       };
848     virtualisation.useEFIBoot =
849       mkOption {
850         type = types.bool;
851         default = false;
852         description = ''
853             If enabled, the virtual machine will provide a EFI boot
854             manager.
855             useEFIBoot is ignored if useBootLoader == false.
856           '';
857         };
859     virtualisation.efi = {
860       OVMF = mkOption {
861         type = types.package;
862         default = (pkgs.OVMF.override {
863           secureBoot = cfg.useSecureBoot;
864         }).fd;
865         defaultText = ''(pkgs.OVMF.override {
866           secureBoot = cfg.useSecureBoot;
867         }).fd'';
868         description = "OVMF firmware package, defaults to OVMF configured with secure boot if needed.";
869       };
871       firmware = mkOption {
872         type = types.path;
873         default = cfg.efi.OVMF.firmware;
874         defaultText = literalExpression "cfg.efi.OVMF.firmware";
875         description = ''
876             Firmware binary for EFI implementation, defaults to OVMF.
877           '';
878       };
880       variables = mkOption {
881         type = types.path;
882         default = cfg.efi.OVMF.variables;
883         defaultText = literalExpression "cfg.efi.OVMF.variables";
884         description = ''
885             Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware.
886             Defaults to OVMF.
887           '';
888       };
890       keepVariables = mkOption {
891         type = types.bool;
892         default = cfg.useBootLoader;
893         defaultText = literalExpression "cfg.useBootLoader";
894         description = "Whether to keep EFI variable values from the generated system image";
895       };
896     };
898     virtualisation.tpm = {
899       enable = mkEnableOption "a TPM device in the virtual machine with a driver, using swtpm";
901       package = mkPackageOption cfg.host.pkgs "swtpm" { };
903       deviceModel = mkOption {
904         type = types.str;
905         default = ({
906           "i686-linux" = "tpm-tis";
907           "x86_64-linux" = "tpm-tis";
908           "ppc64-linux" = "tpm-spapr";
909           "armv7-linux" = "tpm-tis-device";
910           "aarch64-linux" = "tpm-tis-device";
911         }.${pkgs.stdenv.hostPlatform.system} or (throw "Unsupported system for TPM2 emulation in QEMU"));
912         defaultText = ''
913           Based on the guest platform Linux system:
915           - `tpm-tis` for (i686, x86_64)
916           - `tpm-spapr` for ppc64
917           - `tpm-tis-device` for (armv7, aarch64)
918         '';
919         example = "tpm-tis-device";
920         description = "QEMU device model for the TPM, uses the appropriate default based on th guest platform system and the package passed.";
921       };
922     };
924     virtualisation.useDefaultFilesystems =
925       mkOption {
926         type = types.bool;
927         default = true;
928         description = ''
929             If enabled, the boot disk of the virtual machine will be
930             formatted and mounted with the default filesystems for
931             testing. Swap devices and LUKS will be disabled.
933             If disabled, a root filesystem has to be specified and
934             formatted (for example in the initial ramdisk).
935           '';
936       };
938     virtualisation.useSecureBoot =
939       mkOption {
940         type = types.bool;
941         default = false;
942         description = ''
943             Enable Secure Boot support in the EFI firmware.
944           '';
945       };
947     virtualisation.bios =
948       mkOption {
949         type = types.nullOr types.package;
950         default = null;
951         description = ''
952             An alternate BIOS (such as `qboot`) with which to start the VM.
953             Should contain a file named `bios.bin`.
954             If `null`, QEMU's builtin SeaBIOS will be used.
955           '';
956       };
958     virtualisation.useHostCerts =
959       mkOption {
960         type = types.bool;
961         default = false;
962         description = ''
963             If enabled, when `NIX_SSL_CERT_FILE` is set on the host,
964             pass the CA certificates from the host to the VM.
965           '';
966       };
968   };
970   config = {
972     assertions =
973       lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule:
974         [
975           { assertion = rule.from == "guest" -> rule.proto == "tcp";
976             message =
977               ''
978                 Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto:
979                   Guest forwarding supports only TCP connections.
980               '';
981           }
982           { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address;
983             message =
984               ''
985                 Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address:
986                   The address must be in the default VLAN (10.0.2.0/24).
987               '';
988           }
989         ])) ++ [
990           { assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047;
991             message = ''
992               virtualisation.memorySize is above 2047, but qemu is only able to allocate 2047MB RAM on 32bit max.
993             '';
994           }
995           { assertion = cfg.directBoot.enable || cfg.directBoot.initrd == options.virtualisation.directBoot.initrd.default;
996             message =
997               ''
998                 You changed the default of `virtualisation.directBoot.initrd` but you are not
999                 using QEMU direct boot. This initrd will not be used in your current
1000                 boot configuration.
1002                 Either do not mutate `virtualisation.directBoot.initrd` or enable direct boot.
1004                 If you have a more advanced usecase, please open an issue or a pull request.
1005               '';
1006           }
1007           {
1008             assertion = cfg.installBootLoader -> config.system.switch.enable;
1009             message = ''
1010               `system.switch.enable` must be enabled for `virtualisation.installBootLoader` to work.
1011               Please enable it in your configuration.
1012             '';
1013           }
1014         ];
1016     warnings =
1017       optional (cfg.directBoot.enable && cfg.useBootLoader)
1018         ''
1019           You enabled direct boot and a bootloader, QEMU will not boot your bootloader, rendering
1020           `useBootLoader` useless. You might want to disable one of those options.
1021         '';
1023     # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install
1024     # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs.
1025     # Otherwise, we set the proper bootloader device for this.
1026     # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint?
1027     boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice);
1028     boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
1030     boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false);
1032     # After booting, register the closure of the paths in
1033     # `virtualisation.additionalPaths' in the Nix database in the VM.  This
1034     # allows Nix operations to work in the VM.  The path to the
1035     # registration file is passed through the kernel command line to
1036     # allow `system.build.toplevel' to be included.  (If we had a direct
1037     # reference to ${regInfo} here, then we would get a cyclic
1038     # dependency.)
1039     boot.postBootCommands = lib.mkIf config.nix.enable
1040       ''
1041         if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
1042           ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
1043         fi
1044       '';
1046     boot.initrd.availableKernelModules =
1047       optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"
1048       ++ optional (cfg.tpm.enable) "tpm_tis";
1050     virtualisation.additionalPaths = [ config.system.build.toplevel ];
1052     virtualisation.sharedDirectories = {
1053       nix-store = mkIf cfg.mountHostNixStore {
1054         source = builtins.storeDir;
1055         # Always mount this to /nix/.ro-store because we never want to actually
1056         # write to the host Nix Store.
1057         target = "/nix/.ro-store";
1058         securityModel = "none";
1059       };
1060       xchg = {
1061         source = ''"$TMPDIR"/xchg'';
1062         securityModel = "none";
1063         target = "/tmp/xchg";
1064       };
1065       shared = {
1066         source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
1067         target = "/tmp/shared";
1068         securityModel = "none";
1069       };
1070       certs = mkIf cfg.useHostCerts {
1071         source = ''"$TMPDIR"/certs'';
1072         target = "/etc/ssl/certs";
1073         securityModel = "none";
1074       };
1075     };
1077     security.pki.installCACerts = mkIf cfg.useHostCerts false;
1079     virtualisation.qemu.networkingOptions =
1080       let
1081         forwardingOptions = flip concatMapStrings cfg.forwardPorts
1082           ({ proto, from, host, guest }:
1083             if from == "host"
1084               then "hostfwd=${proto}:${host.address}:${toString host.port}-" +
1085                    "${guest.address}:${toString guest.port},"
1086               else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
1087                    "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
1088           );
1089         restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,";
1090       in
1091       [
1092         "-net nic,netdev=user.0,model=virtio"
1093         "-netdev user,id=user.0,${forwardingOptions}${restrictNetworkOption}\"$QEMU_NET_OPTS\""
1094       ];
1096     virtualisation.qemu.options = mkMerge [
1097       (mkIf cfg.qemu.virtioKeyboard [
1098         "-device virtio-keyboard"
1099       ])
1100       (mkIf pkgs.stdenv.hostPlatform.isx86 [
1101         "-usb" "-device usb-tablet,bus=usb-bus.0"
1102       ])
1103       (mkIf pkgs.stdenv.hostPlatform.isAarch [
1104         "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet"
1105       ])
1106       (let
1107         alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9));
1108         # Replace all non-alphanumeric characters with underscores
1109         sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s);
1110       in mkIf cfg.directBoot.enable [
1111         "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}"
1112         "-initrd ${cfg.directBoot.initrd}"
1113         ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
1114       ])
1115       (mkIf cfg.useEFIBoot [
1116         "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
1117         "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS"
1118       ])
1119       (mkIf (cfg.bios != null) [
1120         "-bios ${cfg.bios}/bios.bin"
1121       ])
1122       (mkIf (!cfg.graphics) [
1123         "-nographic"
1124       ])
1125       (mkIf (cfg.tpm.enable) [
1126         "-chardev socket,id=chrtpm,path=\"$NIX_SWTPM_DIR\"/socket"
1127         "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm"
1128         "-device ${cfg.tpm.deviceModel},tpmdev=tpm_dev_0"
1129       ])
1130       (mkIf (pkgs.stdenv.hostPlatform.isx86 && cfg.efi.OVMF.systemManagementModeRequired) [
1131         "-machine" "q35,smm=on"
1132         "-global" "driver=cfi.pflash01,property=secure,value=on"
1133       ])
1134     ];
1136     virtualisation.qemu.drives = mkMerge [
1137       (mkIf (cfg.diskImage != null) [{
1138         name = "root";
1139         file = ''"$NIX_DISK_IMAGE"'';
1140         driveExtraOpts.cache = "writeback";
1141         driveExtraOpts.werror = "report";
1142         deviceExtraOpts.bootindex = "1";
1143         deviceExtraOpts.serial = rootDriveSerialAttr;
1144       }])
1145       (mkIf cfg.useNixStoreImage [{
1146         name = "nix-store";
1147         file = ''"$TMPDIR"/store.img'';
1148         deviceExtraOpts.bootindex = "2";
1149         driveExtraOpts.format = "raw";
1150       }])
1151       (imap0 (idx: _: {
1152         file = "$(pwd)/empty${toString idx}.qcow2";
1153         driveExtraOpts.werror = "report";
1154       }) cfg.emptyDiskImages)
1155     ];
1157     # By default, use mkVMOverride to enable building test VMs (e.g. via
1158     # `nixos-rebuild build-vm`) of a system configuration, where the regular
1159     # value for the `fileSystems' attribute should be disregarded (since those
1160     # filesystems don't necessarily exist in the VM). You can disable this
1161     # override by setting `virtualisation.fileSystems = lib.mkForce { };`.
1162     fileSystems = lib.mkIf (cfg.fileSystems != { }) (mkVMOverride cfg.fileSystems);
1164     virtualisation.fileSystems = let
1165       mkSharedDir = tag: share:
1166         {
1167           name = share.target;
1168           value.device = tag;
1169           value.fsType = "9p";
1170           value.neededForBoot = true;
1171           value.options =
1172             [ "trans=virtio" "version=9p2000.L"  "msize=${toString cfg.msize}" "x-systemd.requires=modprobe@9pnet_virtio.service" ]
1173             ++ lib.optional (tag == "nix-store") "cache=loose";
1174         };
1175     in lib.mkMerge [
1176       (lib.mapAttrs' mkSharedDir cfg.sharedDirectories)
1177       {
1178         "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then {
1179           device = "tmpfs";
1180           fsType = "tmpfs";
1181         } else {
1182           device = cfg.rootDevice;
1183           fsType = "ext4";
1184         });
1185         "/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
1186           device = "tmpfs";
1187           fsType = "tmpfs";
1188           neededForBoot = true;
1189           # Sync with systemd's tmp.mount;
1190           options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ];
1191         };
1192         "/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) (if cfg.writableStore then {
1193           overlay = {
1194             lowerdir = [ "/nix/.ro-store" ];
1195             upperdir = "/nix/.rw-store/upper";
1196             workdir = "/nix/.rw-store/work";
1197           };
1198         } else {
1199           device = "/nix/.ro-store";
1200           options = [ "bind" ];
1201         });
1202         "/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage {
1203           device = "/dev/disk/by-label/${nixStoreFilesystemLabel}";
1204           fsType = "erofs";
1205           neededForBoot = true;
1206           options = [ "ro" ];
1207         };
1208         "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
1209           fsType = "tmpfs";
1210           options = [ "mode=0755" ];
1211           neededForBoot = true;
1212         };
1213         "${config.boot.loader.efi.efiSysMountPoint}" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) {
1214           device = cfg.bootPartition;
1215           fsType = "vfat";
1216         };
1217       }
1218     ];
1220     swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ];
1221     boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) {};
1223     # Don't run ntpd in the guest.  It should get the correct time from KVM.
1224     services.timesyncd.enable = false;
1226     services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
1228     system.build.vm = hostPkgs.runCommand "nixos-vm" {
1229       preferLocalBuild = true;
1230       meta.mainProgram = "run-${config.system.name}-vm";
1231     }
1232       ''
1233         mkdir -p $out/bin
1234         ln -s ${config.system.build.toplevel} $out/system
1235         ln -s ${hostPkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
1236       '';
1238     # When building a regular system configuration, override whatever
1239     # video driver the host uses.
1240     services.xserver.videoDrivers = mkVMOverride [ "modesetting" ];
1241     services.xserver.defaultDepth = mkVMOverride 0;
1242     services.xserver.resolutions = mkVMOverride [ cfg.resolution ];
1243     services.xserver.monitorSection =
1244       ''
1245         # Set a higher refresh rate so that resolutions > 800x600 work.
1246         HorizSync 30-140
1247         VertRefresh 50-160
1248       '';
1250     # Wireless won't work in the VM.
1251     networking.wireless.enable = mkVMOverride false;
1252     services.connman.enable = mkVMOverride false;
1254     # Speed up booting by not waiting for ARP.
1255     networking.dhcpcd.extraConfig = "noarp";
1257     networking.usePredictableInterfaceNames = false;
1259     system.requiredKernelConfig = with config.lib.kernelConfig;
1260       [ (isEnabled "VIRTIO_BLK")
1261         (isEnabled "VIRTIO_PCI")
1262         (isEnabled "VIRTIO_NET")
1263         (isEnabled "EXT4_FS")
1264         (isEnabled "NET_9P_VIRTIO")
1265         (isEnabled "9P_FS")
1266         (isYes "BLK_DEV")
1267         (isYes "PCI")
1268         (isYes "NETDEVICES")
1269         (isYes "NET_CORE")
1270         (isYes "INET")
1271         (isYes "NETWORK_FILESYSTEMS")
1272       ] ++ optionals (!cfg.graphics) [
1273         (isYes "SERIAL_8250_CONSOLE")
1274         (isYes "SERIAL_8250")
1275       ] ++ optionals (cfg.writableStore) [
1276         (isEnabled "OVERLAY_FS")
1277       ];
1279   };
1281   # uses types of services/x11/xserver.nix
1282   meta.buildDocsInSandbox = false;