diff options
Diffstat (limited to 'nixpkgs/nixos/modules/virtualisation/qemu-vm.nix')
-rw-r--r-- | nixpkgs/nixos/modules/virtualisation/qemu-vm.nix | 170 |
1 files changed, 132 insertions, 38 deletions
diff --git a/nixpkgs/nixos/modules/virtualisation/qemu-vm.nix b/nixpkgs/nixos/modules/virtualisation/qemu-vm.nix index 4592ffcfe4d..42e43f5ee02 100644 --- a/nixpkgs/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixpkgs/nixos/modules/virtualisation/qemu-vm.nix @@ -16,11 +16,6 @@ let qemu = config.system.build.qemu or pkgs.qemu_test; - vmName = - if config.networking.hostName == "" - then "noname" - else config.networking.hostName; - cfg = config.virtualisation; consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles; @@ -46,6 +41,13 @@ let description = "Extra options passed to device flag."; }; + name = mkOption { + type = types.nullOr types.str; + default = null; + description = + "A name for the drive. Must be unique in the drives list. Not passed to qemu."; + }; + }; }; @@ -74,6 +76,32 @@ let drivesCmdLine = drives: concatStringsSep " " (imap1 driveCmdline drives); + + # Creates a device name from a 1-based a numerical index, e.g. + # * `driveDeviceName 1` -> `/dev/vda` + # * `driveDeviceName 2` -> `/dev/vdb` + driveDeviceName = idx: + let letter = elemAt lowerChars (idx - 1); + in if cfg.qemu.diskInterface == "scsi" then + "/dev/sd${letter}" + else + "/dev/vd${letter}"; + + lookupDriveDeviceName = driveName: driveList: + (findSingle (drive: drive.name == driveName) + (throw "Drive ${driveName} not found") + (throw "Multiple drives named ${driveName}") driveList).device; + + addDeviceNames = + imap1 (idx: drive: drive // { device = driveDeviceName idx; }); + + efiPrefix = + if (pkgs.stdenv.isi686 || pkgs.stdenv.isx86_64) then "${pkgs.OVMF.fd}/FV/OVMF" + else if pkgs.stdenv.isAarch64 then "${pkgs.OVMF.fd}/FV/AAVMF" + else throw "No EFI firmware available for platform"; + efiFirmware = "${efiPrefix}_CODE.fd"; + efiVarsDefault = "${efiPrefix}_VARS.fd"; + # Shell script to start the VM. startVM = '' @@ -99,10 +127,14 @@ let # A writable boot disk can be booted from automatically. ${qemu}/bin/qemu-img create -f qcow2 -b ${bootDisk}/disk.img $TMPDIR/disk.img || exit 1 + NIX_EFI_VARS=$(readlink -f ''${NIX_EFI_VARS:-${cfg.efiVars}}) + ${if cfg.useEFIBoot then '' - # VM needs a writable flash BIOS. - cp ${bootDisk}/bios.bin $TMPDIR || exit 1 - chmod 0644 $TMPDIR/bios.bin || exit 1 + # VM needs writable EFI vars + if ! test -e "$NIX_EFI_VARS"; then + cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" || exit 1 + chmod 0644 "$NIX_EFI_VARS" || exit 1 + fi '' else '' ''} '' else '' @@ -119,7 +151,7 @@ let # Start QEMU. exec ${qemuBinary qemu} \ - -name ${vmName} \ + -name ${config.system.name} \ -m ${toString config.virtualisation.memorySize} \ -smp ${toString config.virtualisation.cores} \ -device virtio-rng-pci \ @@ -139,6 +171,8 @@ let # Generate a hard disk image containing a /boot partition and GRUB # in the MBR. Used when the `useBootLoader' option is set. + # Uses `runInLinuxVM` to create the image in a throwaway VM. + # See note [Disk layout with `useBootLoader`]. # FIXME: use nixos/lib/make-disk-image.nix. bootDisk = pkgs.vmTools.runInLinuxVM ( @@ -147,21 +181,22 @@ let '' mkdir $out diskImage=$out/disk.img - bootFlash=$out/bios.bin - ${qemu}/bin/qemu-img create -f qcow2 $diskImage "40M" + ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M" ${if cfg.useEFIBoot then '' - cp ${pkgs.OVMF-CSM.fd}/FV/OVMF.fd $bootFlash - chmod 0644 $bootFlash + efiVars=$out/efi-vars.fd + cp ${efiVarsDefault} $efiVars + chmod 0644 $efiVars '' else '' ''} ''; buildInputs = [ pkgs.utillinux ]; - QEMU_OPTS = if cfg.useEFIBoot - then "-pflash $out/bios.bin -nographic -serial pty" - else "-nographic -serial pty"; + QEMU_OPTS = "-nographic -serial stdio -monitor none" + + lib.optionalString cfg.useEFIBoot ( + " -drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}" + + " -drive if=pflash,format=raw,unit=1,file=$efiVars"); } '' - # Create a /boot EFI partition with 40M and arbitrary but fixed GUIDs for reproducibility + # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility ${pkgs.gptfdisk}/bin/sgdisk \ --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \ --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \ @@ -172,6 +207,19 @@ let --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \ --hybrid 2 \ --recompute-chs /dev/vda + + ${optionalString (config.boot.loader.grub.device != "/dev/vda") + # In this throwaway VM, we only have the /dev/vda disk, but the + # actual VM described by `config` (used by `switch-to-configuration` + # below) may set `boot.loader.grub.device` to a different device + # that's nonexistent in the throwaway VM. + # Create a symlink for that device, so that the `grub-install` + # by `switch-to-configuration` will hit /dev/vda anyway. + '' + ln -s /dev/vda ${config.boot.loader.grub.device} + '' + } + ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2 export MTOOLS_SKIP_CHECK=1 ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot @@ -185,6 +233,10 @@ let mkdir /boot mount /dev/vda2 /boot + ${optionalString config.boot.loader.efi.canTouchEfiVariables '' + mount -t efivarfs efivarfs /sys/firmware/efi/efivars + ''} + # This is needed for GRUB 0.97, which doesn't know about virtio devices. mkdir /boot/grub echo '(hd0) /dev/vda' > /boot/grub/device.map @@ -212,7 +264,6 @@ in { imports = [ ../profiles/qemu-guest.nix - ./docker-preloader.nix ]; options = { @@ -237,7 +288,7 @@ in virtualisation.diskImage = mkOption { - default = "./${vmName}.qcow2"; + default = "./${config.system.name}.qcow2"; description = '' Path to the disk image containing the root filesystem. @@ -396,6 +447,7 @@ in mkOption { type = types.listOf (types.submodule driveOpts); description = "Drives passed to qemu."; + apply = addDeviceNames; }; diskInterface = @@ -441,11 +493,53 @@ in ''; }; + virtualisation.efiVars = + mkOption { + default = "./${config.system.name}-efi-vars.fd"; + description = + '' + Path to nvram image containing UEFI variables. The will be created + on startup if it does not exist. + ''; + }; + + virtualisation.bios = + mkOption { + default = null; + type = types.nullOr types.package; + description = + '' + An alternate BIOS (such as <package>qboot</package>) with which to start the VM. + Should contain a file named <literal>bios.bin</literal>. + If <literal>null</literal>, QEMU's builtin SeaBIOS will be used. + ''; + }; + }; config = { - boot.loader.grub.device = mkVMOverride cfg.bootDevice; + # Note [Disk layout with `useBootLoader`] + # + # If `useBootLoader = true`, we configure 2 drives: + # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk + # which has the `/boot` partition and the boot loader. + # Concretely: + # + # * The second drive's image `disk.img` is created in `bootDisk = ...` + # using a throwaway VM. Note that there the disk is always `/dev/vda`, + # even though in the final VM it will be at `/dev/*b`. + # * The disks are attached in `virtualisation.qemu.drives`. + # Their order makes them appear as devices `a`, `b`, etc. + # * `fileSystems."/boot"` is adjusted to be on device `b`. + + # If `useBootLoader`, GRUB goes to the second disk, see + # note [Disk layout with `useBootLoader`]. + boot.loader.grub.device = mkVMOverride ( + if cfg.useBootLoader + then driveDeviceName 2 # second disk + else cfg.bootDevice + ); boot.initrd.extraUtilsCommands = '' @@ -500,8 +594,7 @@ in optional cfg.writableStore "overlay" ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"; - virtualisation.bootDevice = - mkDefault (if cfg.qemu.diskInterface == "scsi" then "/dev/sda" else "/dev/vda"); + virtualisation.bootDevice = mkDefault (driveDeviceName 1); virtualisation.pathsInNixDB = [ config.system.build.toplevel ]; @@ -519,7 +612,11 @@ in ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' ]) (mkIf cfg.useEFIBoot [ - "-pflash $TMPDIR/bios.bin" + "-drive if=pflash,format=raw,unit=0,readonly,file=${efiFirmware}" + "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS" + ]) + (mkIf (cfg.bios != null) [ + "-bios ${cfg.bios}/bios.bin" ]) (mkIf (!cfg.graphics) [ "-nographic" @@ -527,25 +624,22 @@ in ]; virtualisation.qemu.drives = mkMerge [ + [{ + name = "root"; + file = "$NIX_DISK_IMAGE"; + driveExtraOpts.cache = "writeback"; + driveExtraOpts.werror = "report"; + }] (mkIf cfg.useBootLoader [ + # The order of this list determines the device names, see + # note [Disk layout with `useBootLoader`]. { - file = "$NIX_DISK_IMAGE"; - driveExtraOpts.cache = "writeback"; - driveExtraOpts.werror = "report"; - } - { + name = "boot"; file = "$TMPDIR/disk.img"; driveExtraOpts.media = "disk"; deviceExtraOpts.bootindex = "1"; } ]) - (mkIf (!cfg.useBootLoader) [ - { - file = "$NIX_DISK_IMAGE"; - driveExtraOpts.cache = "writeback"; - driveExtraOpts.werror = "report"; - } - ]) (imap0 (idx: _: { file = "$(pwd)/empty${toString idx}.qcow2"; driveExtraOpts.werror = "report"; @@ -593,9 +687,9 @@ in }; } // optionalAttrs cfg.useBootLoader { "/boot" = - { device = "/dev/vdb2"; + # see note [Disk layout with `useBootLoader`] + { device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk` fsType = "vfat"; - options = [ "ro" ]; noCheck = true; # fsck fails on a r/o filesystem }; }); @@ -612,7 +706,7 @@ in '' mkdir -p $out/bin ln -s ${config.system.build.toplevel} $out/system - ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${vmName}-vm + ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm ''; # When building a regular system configuration, override whatever |