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