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, ... }:
11
12with lib;
13
14let
15
16 vmName =
17 if config.networking.hostName == ""
18 then "noname"
19 else config.networking.hostName;
20
21 cfg = config.virtualisation;
22
23 qemuGraphics = if cfg.graphics then "" else "-nographic";
24 kernelConsole = if cfg.graphics then "" else "console=ttyS0";
25 ttys = [ "tty1" "tty2" "tty3" "tty4" "tty5" "tty6" ];
26
27 # Shell script to start the VM.
28 startVM =
29 ''
30 #! ${pkgs.stdenv.shell}
31
32 NIX_DISK_IMAGE=$(readlink -f ''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}})
33
34 if ! test -e "$NIX_DISK_IMAGE"; then
35 ${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \
36 ${toString config.virtualisation.diskSize}M || exit 1
37 fi
38
39 # Create a directory for storing temporary data of the running VM.
40 if [ -z "$TMPDIR" -o -z "$USE_TMPDIR" ]; then
41 TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir)
42 fi
43
44 # Create a directory for exchanging data with the VM.
45 mkdir -p $TMPDIR/xchg
46
47 ${if cfg.useBootLoader then ''
48 # Create a writable copy/snapshot of the boot disk.
49 # A writable boot disk can be booted from automatically.
50 ${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1
51
52 ${if cfg.useEFIBoot then ''
53 # VM needs a writable flash BIOS.
54 cp ${bootDisk}/bios.bin $TMPDIR || exit 1
55 chmod 0644 $TMPDIR/bios.bin || exit 1
56 '' else ''
57 ''}
58 '' else ''
59 ''}
60
61 cd $TMPDIR
62 idx=2
63 extraDisks=""
64 ${flip concatMapStrings cfg.emptyDiskImages (size: ''
65 if ! test -e "empty$idx.qcow2"; then
66 ${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M"
67 fi
68 extraDisks="$extraDisks -drive index=$idx,file=$(pwd)/empty$idx.qcow2,if=${cfg.qemu.diskInterface},werror=report"
69 idx=$((idx + 1))
70 '')}
71
72 # Start QEMU.
73 exec ${pkgs.qemu_kvm}/bin/qemu-kvm \
74 -name ${vmName} \
75 -m ${toString config.virtualisation.memorySize} \
76 ${optionalString (pkgs.stdenv.system == "x86_64-linux") "-cpu kvm64"} \
77 ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \
78 -virtfs local,path=/nix/store,security_model=none,mount_tag=store \
79 -virtfs local,path=$TMPDIR/xchg,security_model=none,mount_tag=xchg \
80 -virtfs local,path=''${SHARED_DIR:-$TMPDIR/xchg},security_model=none,mount_tag=shared \
81 ${if cfg.useBootLoader then ''
82 -drive index=0,id=drive1,file=$NIX_DISK_IMAGE,if=${cfg.qemu.diskInterface},cache=writeback,werror=report \
83 -drive index=1,id=drive2,file=$TMPDIR/disk.img,media=disk \
84 ${if cfg.useEFIBoot then ''
85 -pflash $TMPDIR/bios.bin \
86 '' else ''
87 ''}
88 '' else ''
89 -drive index=0,id=drive1,file=$NIX_DISK_IMAGE,if=${cfg.qemu.diskInterface},cache=writeback,werror=report \
90 -kernel ${config.system.build.toplevel}/kernel \
91 -initrd ${config.system.build.toplevel}/initrd \
92 -append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo} ${kernelConsole} $QEMU_KERNEL_PARAMS" \
93 ''} \
94 $extraDisks \
95 ${qemuGraphics} \
96 ${toString config.virtualisation.qemu.options} \
97 $QEMU_OPTS \
98 $@
99 '';
100
101
102 regInfo = pkgs.runCommand "reginfo"
103 { exportReferencesGraph =
104 map (x: [("closure-" + baseNameOf x) x]) config.virtualisation.pathsInNixDB;
105 buildInputs = [ pkgs.perl ];
106 preferLocalBuild = true;
107 }
108 ''
109 printRegistration=1 perl ${pkgs.pathsFromGraph} closure-* > $out
110 '';
111
112
113 # Generate a hard disk image containing a /boot partition and GRUB
114 # in the MBR. Used when the `useBootLoader' option is set.
115 # FIXME: use nixos/lib/make-disk-image.nix.
116 bootDisk =
117 pkgs.vmTools.runInLinuxVM (
118 pkgs.runCommand "nixos-boot-disk"
119 { preVM =
120 ''
121 mkdir $out
122 diskImage=$out/disk.img
123 bootFlash=$out/bios.bin
124 ${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 $diskImage "40M"
125 ${if cfg.useEFIBoot then ''
126 cp ${pkgs.OVMF-CSM}/FV/OVMF.fd $bootFlash
127 chmod 0644 $bootFlash
128 '' else ''
129 ''}
130 '';
131 buildInputs = [ pkgs.utillinux ];
132 QEMU_OPTS = if cfg.useEFIBoot
133 then "-pflash $out/bios.bin -nographic -serial pty"
134 else "-nographic -serial pty";
135 }
136 ''
137 # Create a /boot EFI partition with 40M
138 ${pkgs.gptfdisk}/bin/sgdisk -G /dev/vda
139 ${pkgs.gptfdisk}/bin/sgdisk -a 1 -n 1:34:2047 -c 1:"BIOS Boot Partition" -t 1:ef02 /dev/vda
140 ${pkgs.gptfdisk}/bin/sgdisk -a 512 -N 2 -c 2:"EFI System" -t 2:ef00 /dev/vda
141 ${pkgs.gptfdisk}/bin/sgdisk -A 1:set:1 /dev/vda
142 ${pkgs.gptfdisk}/bin/sgdisk -A 2:set:2 /dev/vda
143 ${pkgs.gptfdisk}/bin/sgdisk -h 2 /dev/vda
144 ${pkgs.gptfdisk}/bin/sgdisk -C /dev/vda
145 ${pkgs.utillinux}/bin/sfdisk /dev/vda -A 2
146 . /sys/class/block/vda2/uevent
147 mknod /dev/vda2 b $MAJOR $MINOR
148 . /sys/class/block/vda/uevent
149 ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
150 export MTOOLS_SKIP_CHECK=1
151 ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
152
153 # Mount /boot; load necessary modules first.
154 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true
155 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true
156 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true
157 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true
158 ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true
159 mkdir /boot
160 mount /dev/vda2 /boot
161
162 # This is needed for GRUB 0.97, which doesn't know about virtio devices.
163 mkdir /boot/grub
164 echo '(hd0) /dev/vda' > /boot/grub/device.map
165
166 # Install GRUB and generate the GRUB boot menu.
167 touch /etc/NIXOS
168 mkdir -p /nix/var/nix/profiles
169 ${config.system.build.toplevel}/bin/switch-to-configuration boot
170
171 umount /boot
172 '' # */
173 );
174
175in
176
177{
178 imports = [ ../profiles/qemu-guest.nix ];
179
180 options = {
181
182 virtualisation.memorySize =
183 mkOption {
184 default = 384;
185 description =
186 ''
187 Memory size (M) of virtual machine.
188 '';
189 };
190
191 virtualisation.diskSize =
192 mkOption {
193 default = 512;
194 description =
195 ''
196 Disk size (M) of virtual machine.
197 '';
198 };
199
200 virtualisation.diskImage =
201 mkOption {
202 default = "./${vmName}.qcow2";
203 description =
204 ''
205 Path to the disk image containing the root filesystem.
206 The image will be created on startup if it does not
207 exist.
208 '';
209 };
210
211 virtualisation.bootDevice =
212 mkOption {
213 type = types.str;
214 example = "/dev/vda";
215 description =
216 ''
217 The disk to be used for the root filesystem.
218 '';
219 };
220
221 virtualisation.emptyDiskImages =
222 mkOption {
223 default = [];
224 type = types.listOf types.int;
225 description =
226 ''
227 Additional disk images to provide to the VM. The value is
228 a list of size in megabytes of each disk. These disks are
229 writeable by the VM.
230 '';
231 };
232
233 virtualisation.graphics =
234 mkOption {
235 default = true;
236 description =
237 ''
238 Whether to run QEMU with a graphics window, or access
239 the guest computer serial port through the host tty.
240 '';
241 };
242
243 virtualisation.pathsInNixDB =
244 mkOption {
245 default = [];
246 description =
247 ''
248 The list of paths whose closure is registered in the Nix
249 database in the VM. All other paths in the host Nix store
250 appear in the guest Nix store as well, but are considered
251 garbage (because they are not registered in the Nix
252 database in the guest).
253 '';
254 };
255
256 virtualisation.vlans =
257 mkOption {
258 default = [ 1 ];
259 example = [ 1 2 ];
260 description =
261 ''
262 Virtual networks to which the VM is connected. Each
263 number <replaceable>N</replaceable> in this list causes
264 the VM to have a virtual Ethernet interface attached to a
265 separate virtual network on which it will be assigned IP
266 address
267 <literal>192.168.<replaceable>N</replaceable>.<replaceable>M</replaceable></literal>,
268 where <replaceable>M</replaceable> is the index of this VM
269 in the list of VMs.
270 '';
271 };
272
273 virtualisation.writableStore =
274 mkOption {
275 default = false;
276 description =
277 ''
278 If enabled, the Nix store in the VM is made writable by
279 layering a unionfs-fuse/tmpfs filesystem on top of the host's Nix
280 store.
281 '';
282 };
283
284 virtualisation.writableStoreUseTmpfs =
285 mkOption {
286 default = true;
287 description =
288 ''
289 Use a tmpfs for the writable store instead of writing to the VM's
290 own filesystem.
291 '';
292 };
293
294 networking.primaryIPAddress =
295 mkOption {
296 default = "";
297 internal = true;
298 description = "Primary IP address used in /etc/hosts.";
299 };
300
301 virtualisation.qemu = {
302 options =
303 mkOption {
304 type = types.listOf types.unspecified;
305 default = [];
306 example = [ "-vga std" ];
307 description = "Options passed to QEMU.";
308 };
309
310 networkingOptions =
311 mkOption {
312 default = [
313 "-net nic,vlan=0,model=virtio"
314 "-net user,vlan=0\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}"
315 ];
316 type = types.listOf types.str;
317 description = ''
318 Networking-related command-line options that should be passed to qemu.
319 The default is to use userspace networking (slirp).
320
321 If you override this option, be advised to keep
322 ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the default)
323 to keep the default runtime behaviour.
324 '';
325 };
326
327 diskInterface =
328 mkOption {
329 default = "virtio";
330 example = "scsi";
331 type = types.str;
332 description = ''
333 The interface used for the virtual hard disks
334 (<literal>virtio</literal> or <literal>scsi</literal>).
335 '';
336 };
337 };
338
339 virtualisation.useBootLoader =
340 mkOption {
341 default = false;
342 description =
343 ''
344 If enabled, the virtual machine will be booted using the
345 regular boot loader (i.e., GRUB 1 or 2). This allows
346 testing of the boot loader. If
347 disabled (the default), the VM directly boots the NixOS
348 kernel and initial ramdisk, bypassing the boot loader
349 altogether.
350 '';
351 };
352
353 virtualisation.useEFIBoot =
354 mkOption {
355 default = false;
356 description =
357 ''
358 If enabled, the virtual machine will provide a EFI boot
359 manager.
360 useEFIBoot is ignored if useBootLoader == false.
361 '';
362 };
363
364 };
365
366 config = {
367
368 boot.loader.grub.device = mkVMOverride cfg.bootDevice;
369
370 boot.initrd.extraUtilsCommands =
371 ''
372 # We need mke2fs in the initrd.
373 copy_bin_and_libs ${pkgs.e2fsprogs}/bin/mke2fs
374 '';
375
376 boot.initrd.postDeviceCommands =
377 ''
378 # If the disk image appears to be empty, run mke2fs to
379 # initialise.
380 FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
381 if test -z "$FSTYPE"; then
382 mke2fs -t ext4 ${cfg.bootDevice}
383 fi
384 '';
385
386 boot.initrd.postMountCommands =
387 ''
388 # Mark this as a NixOS machine.
389 mkdir -p $targetRoot/etc
390 echo -n > $targetRoot/etc/NIXOS
391
392 # Fix the permissions on /tmp.
393 chmod 1777 $targetRoot/tmp
394
395 mkdir -p $targetRoot/boot
396 '';
397
398 # After booting, register the closure of the paths in
399 # `virtualisation.pathsInNixDB' in the Nix database in the VM. This
400 # allows Nix operations to work in the VM. The path to the
401 # registration file is passed through the kernel command line to
402 # allow `system.build.toplevel' to be included. (If we had a direct
403 # reference to ${regInfo} here, then we would get a cyclic
404 # dependency.)
405 boot.postBootCommands =
406 ''
407 if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then
408 ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]}
409 fi
410 '';
411
412 boot.initrd.availableKernelModules =
413 optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx";
414
415 virtualisation.bootDevice =
416 mkDefault (if cfg.qemu.diskInterface == "scsi" then "/dev/sda" else "/dev/vda");
417
418 virtualisation.pathsInNixDB = [ config.system.build.toplevel ];
419
420 virtualisation.qemu.options = [ "-vga std" "-usbdevice tablet" ];
421
422 # Mount the host filesystem via 9P, and bind-mount the Nix store
423 # of the host into our own filesystem. We use mkVMOverride to
424 # allow this module to be applied to "normal" NixOS system
425 # configuration, where the regular value for the `fileSystems'
426 # attribute should be disregarded for the purpose of building a VM
427 # test image (since those filesystems don't exist in the VM).
428 fileSystems = mkVMOverride (
429 { "/".device = cfg.bootDevice;
430 ${if cfg.writableStore then "/nix/.ro-store" else "/nix/store"} =
431 { device = "store";
432 fsType = "9p";
433 options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
434 neededForBoot = true;
435 };
436 "/tmp/xchg" =
437 { device = "xchg";
438 fsType = "9p";
439 options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ];
440 neededForBoot = true;
441 };
442 "/tmp/shared" =
443 { device = "shared";
444 fsType = "9p";
445 options = [ "trans=virtio" "version=9p2000.L" ];
446 neededForBoot = true;
447 };
448 } // optionalAttrs cfg.writableStore
449 { "/nix/store" =
450 { fsType = "unionfs-fuse";
451 device = "unionfs";
452 options = [ "allow_other" "cow" "nonempty" "chroot=/mnt-root" "max_files=32768" "hide_meta_files" "dirs=/nix/.rw-store=rw:/nix/.ro-store=ro" ];
453 };
454 } // optionalAttrs (cfg.writableStore && cfg.writableStoreUseTmpfs)
455 { "/nix/.rw-store" =
456 { fsType = "tmpfs";
457 options = [ "mode=0755" ];
458 neededForBoot = true;
459 };
460 } // optionalAttrs cfg.useBootLoader
461 { "/boot" =
462 { device = "/dev/vdb2";
463 fsType = "vfat";
464 options = [ "ro" ];
465 noCheck = true; # fsck fails on a r/o filesystem
466 };
467 });
468
469 swapDevices = mkVMOverride [ ];
470 boot.initrd.luks.devices = mkVMOverride {};
471
472 # Don't run ntpd in the guest. It should get the correct time from KVM.
473 services.ntp.enable = false;
474
475 system.build.vm = pkgs.runCommand "nixos-vm" { preferLocalBuild = true; }
476 ''
477 mkdir -p $out/bin
478 ln -s ${config.system.build.toplevel} $out/system
479 ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${vmName}-vm
480 '';
481
482 # When building a regular system configuration, override whatever
483 # video driver the host uses.
484 services.xserver.videoDrivers = mkVMOverride [ "modesetting" ];
485 services.xserver.defaultDepth = mkVMOverride 0;
486 services.xserver.resolutions = mkVMOverride [ { x = 1024; y = 768; } ];
487 services.xserver.monitorSection =
488 ''
489 # Set a higher refresh rate so that resolutions > 800x600 work.
490 HorizSync 30-140
491 VertRefresh 50-160
492 '';
493
494 # Wireless won't work in the VM.
495 networking.wireless.enable = mkVMOverride false;
496 networking.connman.enable = mkVMOverride false;
497
498 # Speed up booting by not waiting for ARP.
499 networking.dhcpcd.extraConfig = "noarp";
500
501 networking.usePredictableInterfaceNames = false;
502
503 system.requiredKernelConfig = with config.lib.kernelConfig;
504 [ (isEnabled "VIRTIO_BLK")
505 (isEnabled "VIRTIO_PCI")
506 (isEnabled "VIRTIO_NET")
507 (isEnabled "EXT4_FS")
508 (isYes "BLK_DEV")
509 (isYes "PCI")
510 (isYes "EXPERIMENTAL")
511 (isYes "NETDEVICES")
512 (isYes "NET_CORE")
513 (isYes "INET")
514 (isYes "NETWORK_FILESYSTEMS")
515 ] ++ optional (!cfg.graphics) [
516 (isYes "SERIAL_8250_CONSOLE")
517 (isYes "SERIAL_8250")
518 ];
519
520 };
521}