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}