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