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