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