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