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