at 16.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 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}