at 15.09-beta 18 kB view raw
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}