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 qemu = cfg.qemu.package;
21
22 consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles;
23
24 driveOpts = { ... }: {
25
26 options = {
27
28 file = mkOption {
29 type = types.str;
30 description = "The file image used for this drive.";
31 };
32
33 driveExtraOpts = mkOption {
34 type = types.attrsOf types.str;
35 default = {};
36 description = "Extra options passed to drive flag.";
37 };
38
39 deviceExtraOpts = mkOption {
40 type = types.attrsOf types.str;
41 default = {};
42 description = "Extra options passed to device flag.";
43 };
44
45 name = mkOption {
46 type = types.nullOr types.str;
47 default = null;
48 description =
49 "A name for the drive. Must be unique in the drives list. Not passed to qemu.";
50 };
51
52 };
53
54 };
55
56 driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
57 let
58 drvId = "drive${toString idx}";
59 mkKeyValue = generators.mkKeyValueDefault {} "=";
60 mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts);
61 driveOpts = mkOpts (driveExtraOpts // {
62 index = idx;
63 id = drvId;
64 "if" = "none";
65 inherit file;
66 });
67 deviceOpts = mkOpts (deviceExtraOpts // {
68 drive = drvId;
69 });
70 device =
71 if cfg.qemu.diskInterface == "scsi" then
72 "-device lsi53c895a -device scsi-hd,${deviceOpts}"
73 else
74 "-device virtio-blk-pci,${deviceOpts}";
75 in
76 "-drive ${driveOpts} ${device}";
77
78 drivesCmdLine = drives: concatStringsSep "\\\n " (imap1 driveCmdline drives);
79
80
81 # Creates a device name from a 1-based a numerical index, e.g.
82 # * `driveDeviceName 1` -> `/dev/vda`
83 # * `driveDeviceName 2` -> `/dev/vdb`
84 driveDeviceName = idx:
85 let letter = elemAt lowerChars (idx - 1);
86 in if cfg.qemu.diskInterface == "scsi" then
87 "/dev/sd${letter}"
88 else
89 "/dev/vd${letter}";
90
91 lookupDriveDeviceName = driveName: driveList:
92 (findSingle (drive: drive.name == driveName)
93 (throw "Drive ${driveName} not found")
94 (throw "Multiple drives named ${driveName}") driveList).device;
95
96 addDeviceNames =
97 imap1 (idx: drive: drive // { device = driveDeviceName idx; });
98
99 efiPrefix =
100 if pkgs.stdenv.hostPlatform.isx86 then "${pkgs.OVMF.fd}/FV/OVMF"
101 else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF"
102 else throw "No EFI firmware available for platform";
103 efiFirmware = "${efiPrefix}_CODE.fd";
104 efiVarsDefault = "${efiPrefix}_VARS.fd";
105
106 # Shell script to start the VM.
107 startVM =
108 ''
109 #! ${pkgs.runtimeShell}
110
111 set -e
112
113 NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}}")
114
115 if ! test -e "$NIX_DISK_IMAGE"; then
116 ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \
117 ${toString config.virtualisation.diskSize}M
118 fi
119
120 # Create a directory for storing temporary data of the running VM.
121 if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then
122 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
123 fi
124
125 ${lib.optionalString cfg.useNixStoreImage
126 ''
127 # Create a writable copy/snapshot of the store image.
128 ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${storeImage}/nixos.qcow2 "$TMPDIR"/store.img
129 ''}
130
131 # Create a directory for exchanging data with the VM.
132 mkdir -p "$TMPDIR/xchg"
133
134 ${lib.optionalString cfg.useBootLoader
135 ''
136 # Create a writable copy/snapshot of the boot disk.
137 # A writable boot disk can be booted from automatically.
138 ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${bootDisk}/disk.img "$TMPDIR/disk.img"
139
140 NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${cfg.efiVars}}")
141
142 ${lib.optionalString cfg.useEFIBoot
143 ''
144 # VM needs writable EFI vars
145 if ! test -e "$NIX_EFI_VARS"; then
146 cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS"
147 chmod 0644 "$NIX_EFI_VARS"
148 fi
149 ''}
150 ''}
151
152 cd "$TMPDIR"
153
154 ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"}
155 ${flip concatMapStrings cfg.emptyDiskImages (size: ''
156 if ! test -e "empty$idx.qcow2"; then
157 ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M"
158 fi
159 idx=$((idx + 1))
160 '')}
161
162 # Start QEMU.
163 exec ${qemu-common.qemuBinary qemu} \
164 -name ${config.system.name} \
165 -m ${toString config.virtualisation.memorySize} \
166 -smp ${toString config.virtualisation.cores} \
167 -device virtio-rng-pci \
168 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
169 ${concatStringsSep " \\\n "
170 (mapAttrsToList
171 (tag: share: "-virtfs local,path=${share.source},security_model=none,mount_tag=${tag}")
172 config.virtualisation.sharedDirectories)} \
173 ${drivesCmdLine config.virtualisation.qemu.drives} \
174 ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \
175 $QEMU_OPTS \
176 "$@"
177 '';
178
179
180 regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
181
182
183 # Generate a hard disk image containing a /boot partition and GRUB
184 # in the MBR. Used when the `useBootLoader' option is set.
185 # Uses `runInLinuxVM` to create the image in a throwaway VM.
186 # See note [Disk layout with `useBootLoader`].
187 # FIXME: use nixos/lib/make-disk-image.nix.
188 bootDisk =
189 pkgs.vmTools.runInLinuxVM (
190 pkgs.runCommand "nixos-boot-disk"
191 { preVM =
192 ''
193 mkdir $out
194 diskImage=$out/disk.img
195 ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
196 ${if cfg.useEFIBoot then ''
197 efiVars=$out/efi-vars.fd
198 cp ${efiVarsDefault} $efiVars
199 chmod 0644 $efiVars
200 '' else ""}
201 '';
202 buildInputs = [ pkgs.util-linux ];
203 QEMU_OPTS = "-nographic -serial stdio -monitor none"
204 + lib.optionalString cfg.useEFIBoot (
205 " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
206 + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
207 }
208 ''
209 # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
210 ${pkgs.gptfdisk}/bin/sgdisk \
211 --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
212 --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
213 --attributes=1:set:1 \
214 --attributes=2:set:2 \
215 --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \
216 --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
217 --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
218 --hybrid 2 \
219 --recompute-chs /dev/vda
220
221 ${optionalString (config.boot.loader.grub.device != "/dev/vda")
222 # In this throwaway VM, we only have the /dev/vda disk, but the
223 # actual VM described by `config` (used by `switch-to-configuration`
224 # below) may set `boot.loader.grub.device` to a different device
225 # that's nonexistent in the throwaway VM.
226 # Create a symlink for that device, so that the `grub-install`
227 # by `switch-to-configuration` will hit /dev/vda anyway.
228 ''
229 ln -s /dev/vda ${config.boot.loader.grub.device}
230 ''
231 }
232
233 ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
234 export MTOOLS_SKIP_CHECK=1
235 ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
236
237 # Mount /boot; load necessary modules first.
238 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true
239 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true
240 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true
241 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true
242 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true
243 mkdir /boot
244 mount /dev/vda2 /boot
245
246 ${optionalString config.boot.loader.efi.canTouchEfiVariables ''
247 mount -t efivarfs efivarfs /sys/firmware/efi/efivars
248 ''}
249
250 # This is needed for GRUB 0.97, which doesn't know about virtio devices.
251 mkdir /boot/grub
252 echo '(hd0) /dev/vda' > /boot/grub/device.map
253
254 # This is needed for systemd-boot to find ESP, and udev is not available here to create this
255 mkdir -p /dev/block
256 ln -s /dev/vda2 /dev/block/254:2
257
258 # Set up system profile (normally done by nixos-rebuild / nix-env --set)
259 mkdir -p /nix/var/nix/profiles
260 ln -s ${config.system.build.toplevel} /nix/var/nix/profiles/system-1-link
261 ln -s /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system
262
263 # Install bootloader
264 touch /etc/NIXOS
265 export NIXOS_INSTALL_BOOTLOADER=1
266 ${config.system.build.toplevel}/bin/switch-to-configuration boot
267
268 umount /boot
269 '' # */
270 );
271
272 storeImage = import ../../lib/make-disk-image.nix {
273 inherit pkgs config lib;
274 additionalPaths = [ regInfo ];
275 format = "qcow2";
276 onlyNixStore = true;
277 partitionTableType = "none";
278 installBootLoader = false;
279 diskSize = "auto";
280 additionalSpace = "0M";
281 copyChannel = false;
282 };
283
284in
285
286{
287 imports = [
288 ../profiles/qemu-guest.nix
289 (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
290 ];
291
292 options = {
293
294 virtualisation.fileSystems = options.fileSystems;
295
296 virtualisation.memorySize =
297 mkOption {
298 type = types.ints.positive;
299 default = 1024;
300 description =
301 ''
302 The memory size in megabytes of the virtual machine.
303 '';
304 };
305
306 virtualisation.msize =
307 mkOption {
308 type = types.ints.positive;
309 default = pkgs.vmTools.default9PMsizeBytes;
310 description =
311 ''
312 The msize (maximum packet size) option passed to 9p file systems, in
313 bytes. Increasing this should increase performance significantly,
314 at the cost of higher RAM usage.
315 '';
316 };
317
318 virtualisation.diskSize =
319 mkOption {
320 type = types.nullOr types.ints.positive;
321 default = 1024;
322 description =
323 ''
324 The disk size in megabytes of the virtual machine.
325 '';
326 };
327
328 virtualisation.diskImage =
329 mkOption {
330 type = types.str;
331 default = "./${config.system.name}.qcow2";
332 description =
333 ''
334 Path to the disk image containing the root filesystem.
335 The image will be created on startup if it does not
336 exist.
337 '';
338 };
339
340 virtualisation.bootDevice =
341 mkOption {
342 type = types.path;
343 example = "/dev/vda";
344 description =
345 ''
346 The disk to be used for the root filesystem.
347 '';
348 };
349
350 virtualisation.emptyDiskImages =
351 mkOption {
352 type = types.listOf types.ints.positive;
353 default = [];
354 description =
355 ''
356 Additional disk images to provide to the VM. The value is
357 a list of size in megabytes of each disk. These disks are
358 writeable by the VM.
359 '';
360 };
361
362 virtualisation.graphics =
363 mkOption {
364 type = types.bool;
365 default = true;
366 description =
367 ''
368 Whether to run QEMU with a graphics window, or in nographic mode.
369 Serial console will be enabled on both settings, but this will
370 change the preferred console.
371 '';
372 };
373
374 virtualisation.resolution =
375 mkOption {
376 type = options.services.xserver.resolutions.type.nestedTypes.elemType;
377 default = { x = 1024; y = 768; };
378 description =
379 ''
380 The resolution of the virtual machine display.
381 '';
382 };
383
384 virtualisation.cores =
385 mkOption {
386 type = types.ints.positive;
387 default = 1;
388 description =
389 ''
390 Specify the number of cores the guest is permitted to use.
391 The number can be higher than the available cores on the
392 host system.
393 '';
394 };
395
396 virtualisation.sharedDirectories =
397 mkOption {
398 type = types.attrsOf
399 (types.submodule {
400 options.source = mkOption {
401 type = types.str;
402 description = "The path of the directory to share, can be a shell variable";
403 };
404 options.target = mkOption {
405 type = types.path;
406 description = "The mount point of the directory inside the virtual machine";
407 };
408 });
409 default = { };
410 example = {
411 my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; };
412 };
413 description =
414 ''
415 An attributes set of directories that will be shared with the
416 virtual machine using VirtFS (9P filesystem over VirtIO).
417 The attribute name will be used as the 9P mount tag.
418 '';
419 };
420
421 virtualisation.additionalPaths =
422 mkOption {
423 type = types.listOf types.path;
424 default = [];
425 description =
426 ''
427 A list of paths whose closure should be made available to
428 the VM.
429
430 When 9p is used, the closure is registered in the Nix
431 database in the VM. All other paths in the host Nix store
432 appear in the guest Nix store as well, but are considered
433 garbage (because they are not registered in the Nix
434 database of the guest).
435
436 When <option>virtualisation.useNixStoreImage</option> is
437 set, the closure is copied to the Nix store image.
438 '';
439 };
440
441 virtualisation.forwardPorts = mkOption {
442 type = types.listOf
443 (types.submodule {
444 options.from = mkOption {
445 type = types.enum [ "host" "guest" ];
446 default = "host";
447 description =
448 ''
449 Controls the direction in which the ports are mapped:
450
451 - <literal>"host"</literal> means traffic from the host ports
452 is forwarded to the given guest port.
453
454 - <literal>"guest"</literal> means traffic from the guest ports
455 is forwarded to the given host port.
456 '';
457 };
458 options.proto = mkOption {
459 type = types.enum [ "tcp" "udp" ];
460 default = "tcp";
461 description = "The protocol to forward.";
462 };
463 options.host.address = mkOption {
464 type = types.str;
465 default = "";
466 description = "The IPv4 address of the host.";
467 };
468 options.host.port = mkOption {
469 type = types.port;
470 description = "The host port to be mapped.";
471 };
472 options.guest.address = mkOption {
473 type = types.str;
474 default = "";
475 description = "The IPv4 address on the guest VLAN.";
476 };
477 options.guest.port = mkOption {
478 type = types.port;
479 description = "The guest port to be mapped.";
480 };
481 });
482 default = [];
483 example = lib.literalExpression
484 ''
485 [ # forward local port 2222 -> 22, to ssh into the VM
486 { from = "host"; host.port = 2222; guest.port = 22; }
487
488 # forward local port 80 -> 10.0.2.10:80 in the VLAN
489 { from = "guest";
490 guest.address = "10.0.2.10"; guest.port = 80;
491 host.address = "127.0.0.1"; host.port = 80;
492 }
493 ]
494 '';
495 description =
496 ''
497 When using the SLiRP user networking (default), this option allows to
498 forward ports to/from the host/guest.
499
500 <warning><para>
501 If the NixOS firewall on the virtual machine is enabled, you also
502 have to open the guest ports to enable the traffic between host and
503 guest.
504 </para></warning>
505
506 <note><para>Currently QEMU supports only IPv4 forwarding.</para></note>
507 '';
508 };
509
510 virtualisation.vlans =
511 mkOption {
512 type = types.listOf types.ints.unsigned;
513 default = [ 1 ];
514 example = [ 1 2 ];
515 description =
516 ''
517 Virtual networks to which the VM is connected. Each
518 number <replaceable>N</replaceable> in this list causes
519 the VM to have a virtual Ethernet interface attached to a
520 separate virtual network on which it will be assigned IP
521 address
522 <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>,
523 where <replaceable>M</replaceable> is the index of this VM
524 in the list of VMs.
525 '';
526 };
527
528 virtualisation.writableStore =
529 mkOption {
530 type = types.bool;
531 default = true; # FIXME
532 description =
533 ''
534 If enabled, the Nix store in the VM is made writable by
535 layering an overlay filesystem on top of the host's Nix
536 store.
537 '';
538 };
539
540 virtualisation.writableStoreUseTmpfs =
541 mkOption {
542 type = types.bool;
543 default = true;
544 description =
545 ''
546 Use a tmpfs for the writable store instead of writing to the VM's
547 own filesystem.
548 '';
549 };
550
551 networking.primaryIPAddress =
552 mkOption {
553 type = types.str;
554 default = "";
555 internal = true;
556 description = "Primary IP address used in /etc/hosts.";
557 };
558
559 virtualisation.qemu = {
560 package =
561 mkOption {
562 type = types.package;
563 default = pkgs.qemu_kvm;
564 example = "pkgs.qemu_test";
565 description = "QEMU package to use.";
566 };
567
568 options =
569 mkOption {
570 type = types.listOf types.str;
571 default = [];
572 example = [ "-vga std" ];
573 description = "Options passed to QEMU.";
574 };
575
576 consoles = mkOption {
577 type = types.listOf types.str;
578 default = let
579 consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ];
580 in if cfg.graphics then consoles else reverseList consoles;
581 example = [ "console=tty1" ];
582 description = ''
583 The output console devices to pass to the kernel command line via the
584 <literal>console</literal> parameter, the primary console is the last
585 item of this list.
586
587 By default it enables both serial console and
588 <literal>tty0</literal>. The preferred console (last one) is based on
589 the value of <option>virtualisation.graphics</option>.
590 '';
591 };
592
593 networkingOptions =
594 mkOption {
595 type = types.listOf types.str;
596 default = [ ];
597 example = [
598 "-net nic,netdev=user.0,model=virtio"
599 "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
600 ];
601 description = ''
602 Networking-related command-line options that should be passed to qemu.
603 The default is to use userspace networking (SLiRP).
604
605 If you override this option, be advised to keep
606 ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example)
607 to keep the default runtime behaviour.
608 '';
609 };
610
611 drives =
612 mkOption {
613 type = types.listOf (types.submodule driveOpts);
614 description = "Drives passed to qemu.";
615 apply = addDeviceNames;
616 };
617
618 diskInterface =
619 mkOption {
620 type = types.enum [ "virtio" "scsi" "ide" ];
621 default = "virtio";
622 example = "scsi";
623 description = "The interface used for the virtual hard disks.";
624 };
625
626 guestAgent.enable =
627 mkOption {
628 type = types.bool;
629 default = true;
630 description = ''
631 Enable the Qemu guest agent.
632 '';
633 };
634 };
635
636 virtualisation.useNixStoreImage =
637 mkOption {
638 type = types.bool;
639 default = false;
640 description = ''
641 Build and use a disk image for the Nix store, instead of
642 accessing the host's one through 9p.
643
644 For applications which do a lot of reads from the store,
645 this can drastically improve performance, but at the cost of
646 disk space and image build time.
647 '';
648 };
649
650 virtualisation.useBootLoader =
651 mkOption {
652 type = types.bool;
653 default = false;
654 description =
655 ''
656 If enabled, the virtual machine will be booted using the
657 regular boot loader (i.e., GRUB 1 or 2). This allows
658 testing of the boot loader. If
659 disabled (the default), the VM directly boots the NixOS
660 kernel and initial ramdisk, bypassing the boot loader
661 altogether.
662 '';
663 };
664
665 virtualisation.useEFIBoot =
666 mkOption {
667 type = types.bool;
668 default = false;
669 description =
670 ''
671 If enabled, the virtual machine will provide a EFI boot
672 manager.
673 useEFIBoot is ignored if useBootLoader == false.
674 '';
675 };
676
677 virtualisation.efiVars =
678 mkOption {
679 type = types.str;
680 default = "./${config.system.name}-efi-vars.fd";
681 description =
682 ''
683 Path to nvram image containing UEFI variables. The will be created
684 on startup if it does not exist.
685 '';
686 };
687
688 virtualisation.bios =
689 mkOption {
690 type = types.nullOr types.package;
691 default = null;
692 description =
693 ''
694 An alternate BIOS (such as <package>qboot</package>) with which to start the VM.
695 Should contain a file named <literal>bios.bin</literal>.
696 If <literal>null</literal>, QEMU's builtin SeaBIOS will be used.
697 '';
698 };
699
700 };
701
702 config = {
703
704 assertions =
705 lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule:
706 [
707 { assertion = rule.from == "guest" -> rule.proto == "tcp";
708 message =
709 ''
710 Invalid virtualisation.forwardPorts.<entry ${toString i}>.proto:
711 Guest forwarding supports only TCP connections.
712 '';
713 }
714 { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address;
715 message =
716 ''
717 Invalid virtualisation.forwardPorts.<entry ${toString i}>.guest.address:
718 The address must be in the default VLAN (10.0.2.0/24).
719 '';
720 }
721 ]));
722
723 # Note [Disk layout with `useBootLoader`]
724 #
725 # If `useBootLoader = true`, we configure 2 drives:
726 # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
727 # which has the `/boot` partition and the boot loader.
728 # Concretely:
729 #
730 # * The second drive's image `disk.img` is created in `bootDisk = ...`
731 # using a throwaway VM. Note that there the disk is always `/dev/vda`,
732 # even though in the final VM it will be at `/dev/*b`.
733 # * The disks are attached in `virtualisation.qemu.drives`.
734 # Their order makes them appear as devices `a`, `b`, etc.
735 # * `fileSystems."/boot"` is adjusted to be on device `b`.
736
737 # If `useBootLoader`, GRUB goes to the second disk, see
738 # note [Disk layout with `useBootLoader`].
739 boot.loader.grub.device = mkVMOverride (
740 if cfg.useBootLoader
741 then driveDeviceName 2 # second disk
742 else cfg.bootDevice
743 );
744 boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
745
746 boot.initrd.extraUtilsCommands =
747 ''
748 # We need mke2fs in the initrd.
749 copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
750 '';
751
752 boot.initrd.postDeviceCommands =
753 ''
754 # If the disk image appears to be empty, run mke2fs to
755 # initialise.
756 FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
757 if test -z "$FSTYPE"; then
758 mke2fs -t ext4 ${cfg.bootDevice}
759 fi
760 '';
761
762 boot.initrd.postMountCommands =
763 ''
764 # Mark this as a NixOS machine.
765 mkdir -p $targetRoot/etc
766 echo -n > $targetRoot/etc/NIXOS
767
768 # Fix the permissions on /tmp.
769 chmod 1777 $targetRoot/tmp
770
771 mkdir -p $targetRoot/boot
772
773 ${optionalString cfg.writableStore ''
774 echo "mounting overlay filesystem on /nix/store..."
775 mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
776 mount -t overlay overlay $targetRoot/nix/store \
777 -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail
778 ''}
779 '';
780
781 # After booting, register the closure of the paths in
782 # `virtualisation.additionalPaths' in the Nix database in the VM. This
783 # allows Nix operations to work in the VM. The path to the
784 # registration file is passed through the kernel command line to
785 # allow `system.build.toplevel' to be included. (If we had a direct
786 # reference to ${regInfo} here, then we would get a cyclic
787 # dependency.)
788 boot.postBootCommands =
789 ''
790 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
791 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
792 fi
793 '';
794
795 boot.initrd.availableKernelModules =
796 optional cfg.writableStore "overlay"
797 ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx";
798
799 virtualisation.bootDevice = mkDefault (driveDeviceName 1);
800
801 virtualisation.additionalPaths = [ config.system.build.toplevel ];
802
803 virtualisation.sharedDirectories = {
804 nix-store = mkIf (!cfg.useNixStoreImage) {
805 source = builtins.storeDir;
806 target = "/nix/store";
807 };
808 xchg = {
809 source = ''"$TMPDIR"/xchg'';
810 target = "/tmp/xchg";
811 };
812 shared = {
813 source = ''"''${SHARED_DIR:-$TMPDIR/xchg}"'';
814 target = "/tmp/shared";
815 };
816 };
817
818 virtualisation.qemu.networkingOptions =
819 let
820 forwardingOptions = flip concatMapStrings cfg.forwardPorts
821 ({ proto, from, host, guest }:
822 if from == "host"
823 then "hostfwd=${proto}:${host.address}:${toString host.port}-" +
824 "${guest.address}:${toString guest.port},"
825 else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
826 "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
827 );
828 in
829 [
830 "-net nic,netdev=user.0,model=virtio"
831 "-netdev user,id=user.0,${forwardingOptions}\"$QEMU_NET_OPTS\""
832 ];
833
834 # FIXME: Consolidate this one day.
835 virtualisation.qemu.options = mkMerge [
836 (mkIf pkgs.stdenv.hostPlatform.isx86 [
837 "-usb" "-device usb-tablet,bus=usb-bus.0"
838 ])
839 (mkIf (pkgs.stdenv.isAarch32 || pkgs.stdenv.isAarch64) [
840 "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet"
841 ])
842 (mkIf (!cfg.useBootLoader) [
843 "-kernel ${config.system.build.toplevel}/kernel"
844 "-initrd ${config.system.build.toplevel}/initrd"
845 ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"''
846 ])
847 (mkIf cfg.useEFIBoot [
848 "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
849 "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
850 ])
851 (mkIf (cfg.bios != null) [
852 "-bios ${cfg.bios}/bios.bin"
853 ])
854 (mkIf (!cfg.graphics) [
855 "-nographic"
856 ])
857 ];
858
859 virtualisation.qemu.drives = mkMerge [
860 [{
861 name = "root";
862 file = ''"$NIX_DISK_IMAGE"'';
863 driveExtraOpts.cache = "writeback";
864 driveExtraOpts.werror = "report";
865 }]
866 (mkIf cfg.useNixStoreImage [{
867 name = "nix-store";
868 file = ''"$TMPDIR"/store.img'';
869 deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2";
870 }])
871 (mkIf cfg.useBootLoader [
872 # The order of this list determines the device names, see
873 # note [Disk layout with `useBootLoader`].
874 {
875 name = "boot";
876 file = ''"$TMPDIR"/disk.img'';
877 driveExtraOpts.media = "disk";
878 deviceExtraOpts.bootindex = "1";
879 }
880 ])
881 (imap0 (idx: _: {
882 file = "$(pwd)/empty${toString idx}.qcow2";
883 driveExtraOpts.werror = "report";
884 }) cfg.emptyDiskImages)
885 ];
886
887 # Mount the host filesystem via 9P, and bind-mount the Nix store
888 # of the host into our own filesystem. We use mkVMOverride to
889 # allow this module to be applied to "normal" NixOS system
890 # configuration, where the regular value for the `fileSystems'
891 # attribute should be disregarded for the purpose of building a VM
892 # test image (since those filesystems don't exist in the VM).
893 fileSystems =
894 let
895 mkSharedDir = tag: share:
896 {
897 name =
898 if tag == "nix-store" && cfg.writableStore
899 then "/nix/.ro-store"
900 else share.target;
901 value.device = tag;
902 value.fsType = "9p";
903 value.neededForBoot = true;
904 value.options =
905 [ "trans=virtio" "version=9p2000.L" "msize=${toString cfg.msize}" ]
906 ++ lib.optional (tag == "nix-store") "cache=loose";
907 };
908 in
909 mkVMOverride (cfg.fileSystems //
910 {
911 "/".device = cfg.bootDevice;
912
913 "/tmp" = mkIf config.boot.tmpOnTmpfs
914 { device = "tmpfs";
915 fsType = "tmpfs";
916 neededForBoot = true;
917 # Sync with systemd's tmp.mount;
918 options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
919 };
920
921 "/nix/${if cfg.writableStore then ".ro-store" else "store"}" =
922 mkIf cfg.useNixStoreImage
923 { device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
924 neededForBoot = true;
925 options = [ "ro" ];
926 };
927
928 "/nix/.rw-store" = mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs)
929 { fsType = "tmpfs";
930 options = [ "mode=0755" ];
931 neededForBoot = true;
932 };
933
934 "/boot" = mkIf cfg.useBootLoader
935 # see note [Disk layout with `useBootLoader`]
936 { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk`
937 fsType = "vfat";
938 noCheck = true; # fsck fails on a r/o filesystem
939 };
940 } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories);
941
942 swapDevices = mkVMOverride [ ];
943 boot.initrd.luks.devices = mkVMOverride {};
944
945 # Don't run ntpd in the guest. It should get the correct time from KVM.
946 services.timesyncd.enable = false;
947
948 services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
949
950 system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; }
951 ''
952 mkdir -p $out/bin
953 ln -s ${config.system.build.toplevel} $out/system
954 ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
955 '';
956
957 # When building a regular system configuration, override whatever
958 # video driver the host uses.
959 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ];
960 services.xserver.defaultDepth = mkVMOverride 0;
961 services.xserver.resolutions = mkVMOverride [ cfg.resolution ];
962 services.xserver.monitorSection =
963 ''
964 # Set a higher refresh rate so that resolutions > 800x600 work.
965 HorizSync 30-140
966 VertRefresh 50-160
967 '';
968
969 # Wireless won't work in the VM.
970 networking.wireless.enable = mkVMOverride false;
971 services.connman.enable = mkVMOverride false;
972
973 # Speed up booting by not waiting for ARP.
974 networking.dhcpcd.extraConfig = "noarp";
975
976 networking.usePredictableInterfaceNames = false;
977
978 system.requiredKernelConfig = with config.lib.kernelConfig;
979 [ (isEnabled "VIRTIO_BLK")
980 (isEnabled "VIRTIO_PCI")
981 (isEnabled "VIRTIO_NET")
982 (isEnabled "EXT4_FS")
983 (isEnabled "NET_9P_VIRTIO")
984 (isEnabled "9P_FS")
985 (isYes "BLK_DEV")
986 (isYes "PCI")
987 (isYes "NETDEVICES")
988 (isYes "NET_CORE")
989 (isYes "INET")
990 (isYes "NETWORK_FILESYSTEMS")
991 ] ++ optionals (!cfg.graphics) [
992 (isYes "SERIAL_8250_CONSOLE")
993 (isYes "SERIAL_8250")
994 ] ++ optionals (cfg.writableStore) [
995 (isEnabled "OVERLAY_FS")
996 ];
997
998 };
999}