diff --git a/nixos/maintainers/scripts/ec2/amazon-image.nix b/nixos/maintainers/scripts/ec2/amazon-image.nix index 1b3724bfc170..9d0e7f5883e0 100644 --- a/nixos/maintainers/scripts/ec2/amazon-image.nix +++ b/nixos/maintainers/scripts/ec2/amazon-image.nix @@ -1,12 +1,23 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let - inherit (lib) mkOption optionalString types versionAtLeast; + inherit (lib) + mkOption + optionalString + types + versionAtLeast + ; inherit (lib.options) literalExpression; cfg = config.amazonImage; amiBootMode = if config.ec2.efi then "uefi" else "legacy-bios"; -in { +in +{ imports = [ ../../../modules/virtualisation/amazon-image.nix ]; @@ -14,11 +25,11 @@ in { # experience, which prior to 4.15 was 255. # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html#timeout-nvme-ebs-volumes config.boot.kernelParams = - let timeout = - if versionAtLeast config.boot.kernelPackages.kernel.version "4.15" - then "4294967295" - else "255"; - in [ "nvme_core.io_timeout=${timeout}" ]; + let + timeout = + if versionAtLeast config.boot.kernelPackages.kernel.version "4.15" then "4294967295" else "255"; + in + [ "nvme_core.io_timeout=${timeout}" ]; options.amazonImage = { name = mkOption { @@ -34,7 +45,7 @@ in { } ] ''; - default = []; + default = [ ]; description = '' This option lists files to be copied to fixed locations in the generated image. Glob patterns work. @@ -49,15 +60,19 @@ in { }; format = mkOption { - type = types.enum [ "raw" "qcow2" "vpc" ]; + type = types.enum [ + "raw" + "qcow2" + "vpc" + ]; default = "vpc"; description = "The image format to output"; }; }; - config.system.build.amazonImage = let - configFile = pkgs.writeText "configuration.nix" - '' + config.system.build.amazonImage = + let + configFile = pkgs.writeText "configuration.nix" '' { modulesPath, ... }: { imports = [ "''${modulesPath}/virtualisation/amazon-image.nix" ]; ${optionalString config.ec2.efi '' @@ -70,91 +85,102 @@ in { } ''; - zfsBuilder = import ../../../lib/make-multi-disk-zfs-image.nix { - inherit lib config configFile pkgs; - inherit (cfg) contents format name; + zfsBuilder = import ../../../lib/make-multi-disk-zfs-image.nix { + inherit + lib + config + configFile + pkgs + ; + inherit (cfg) contents format name; - includeChannel = true; + includeChannel = true; - bootSize = 1000; # 1G is the minimum EBS volume + bootSize = 1000; # 1G is the minimum EBS volume - rootSize = cfg.sizeMB; - rootPoolProperties = { - ashift = 12; - autoexpand = "on"; + rootSize = cfg.sizeMB; + rootPoolProperties = { + ashift = 12; + autoexpand = "on"; + }; + + datasets = config.ec2.zfs.datasets; + + postVM = '' + extension=''${rootDiskImage##*.} + friendlyName=$out/${cfg.name} + rootDisk="$friendlyName.root.$extension" + bootDisk="$friendlyName.boot.$extension" + mv "$rootDiskImage" "$rootDisk" + mv "$bootDiskImage" "$bootDisk" + + mkdir -p $out/nix-support + echo "file ${cfg.format} $bootDisk" >> $out/nix-support/hydra-build-products + echo "file ${cfg.format} $rootDisk" >> $out/nix-support/hydra-build-products + + ${pkgs.jq}/bin/jq -n \ + --arg system_label ${lib.escapeShellArg config.system.nixos.label} \ + --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \ + --arg root_logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$rootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ + --arg boot_logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$bootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ + --arg boot_mode "${amiBootMode}" \ + --arg root "$rootDisk" \ + --arg boot "$bootDisk" \ + '{} + | .label = $system_label + | .boot_mode = $boot_mode + | .system = $system + | .disks.boot.logical_bytes = $boot_logical_bytes + | .disks.boot.file = $boot + | .disks.root.logical_bytes = $root_logical_bytes + | .disks.root.file = $root + ' > $out/nix-support/image-info.json + ''; }; - datasets = config.ec2.zfs.datasets; + extBuilder = import ../../../lib/make-disk-image.nix { + inherit + lib + config + configFile + pkgs + ; - postVM = '' - extension=''${rootDiskImage##*.} - friendlyName=$out/${cfg.name} - rootDisk="$friendlyName.root.$extension" - bootDisk="$friendlyName.boot.$extension" - mv "$rootDiskImage" "$rootDisk" - mv "$bootDiskImage" "$bootDisk" + inherit (cfg) contents format name; - mkdir -p $out/nix-support - echo "file ${cfg.format} $bootDisk" >> $out/nix-support/hydra-build-products - echo "file ${cfg.format} $rootDisk" >> $out/nix-support/hydra-build-products + fsType = "ext4"; + partitionTableType = if config.ec2.efi then "efi" else "legacy+gpt"; - ${pkgs.jq}/bin/jq -n \ - --arg system_label ${lib.escapeShellArg config.system.nixos.label} \ - --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \ - --arg root_logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$rootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ - --arg boot_logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$bootDisk" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ - --arg boot_mode "${amiBootMode}" \ - --arg root "$rootDisk" \ - --arg boot "$bootDisk" \ - '{} - | .label = $system_label - | .boot_mode = $boot_mode - | .system = $system - | .disks.boot.logical_bytes = $boot_logical_bytes - | .disks.boot.file = $boot - | .disks.root.logical_bytes = $root_logical_bytes - | .disks.root.file = $root - ' > $out/nix-support/image-info.json - ''; - }; + diskSize = cfg.sizeMB; - extBuilder = import ../../../lib/make-disk-image.nix { - inherit lib config configFile pkgs; + postVM = '' + extension=''${diskImage##*.} + friendlyName=$out/${cfg.name}.$extension + mv "$diskImage" "$friendlyName" + diskImage=$friendlyName - inherit (cfg) contents format name; + mkdir -p $out/nix-support + echo "file ${cfg.format} $diskImage" >> $out/nix-support/hydra-build-products - fsType = "ext4"; - partitionTableType = if config.ec2.efi then "efi" else "legacy+gpt"; - - diskSize = cfg.sizeMB; - - postVM = '' - extension=''${diskImage##*.} - friendlyName=$out/${cfg.name}.$extension - mv "$diskImage" "$friendlyName" - diskImage=$friendlyName - - mkdir -p $out/nix-support - echo "file ${cfg.format} $diskImage" >> $out/nix-support/hydra-build-products - - ${pkgs.jq}/bin/jq -n \ - --arg system_label ${lib.escapeShellArg config.system.nixos.label} \ - --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \ - --arg logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$diskImage" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ - --arg boot_mode "${amiBootMode}" \ - --arg file "$diskImage" \ - '{} - | .label = $system_label - | .boot_mode = $boot_mode - | .system = $system - | .logical_bytes = $logical_bytes - | .file = $file - | .disks.root.logical_bytes = $logical_bytes - | .disks.root.file = $file - ' > $out/nix-support/image-info.json - ''; - }; - in if config.ec2.zfs.enable then zfsBuilder else extBuilder; + ${pkgs.jq}/bin/jq -n \ + --arg system_label ${lib.escapeShellArg config.system.nixos.label} \ + --arg system ${lib.escapeShellArg pkgs.stdenv.hostPlatform.system} \ + --arg logical_bytes "$(${pkgs.qemu_kvm}/bin/qemu-img info --output json "$diskImage" | ${pkgs.jq}/bin/jq '."virtual-size"')" \ + --arg boot_mode "${amiBootMode}" \ + --arg file "$diskImage" \ + '{} + | .label = $system_label + | .boot_mode = $boot_mode + | .system = $system + | .logical_bytes = $logical_bytes + | .file = $file + | .disks.root.logical_bytes = $logical_bytes + | .disks.root.file = $file + ' > $out/nix-support/image-info.json + ''; + }; + in + if config.ec2.zfs.enable then zfsBuilder else extBuilder; meta.maintainers = with lib.maintainers; [ arianvp ]; } diff --git a/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix b/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix index 9799f333aec0..72f123b16229 100644 --- a/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix +++ b/nixos/maintainers/scripts/openstack/openstack-image-zfs.nix @@ -1,6 +1,11 @@ # nix-build '' -A config.system.build.openstackImage --arg configuration "{ imports = [ ./nixos/maintainers/scripts/openstack/openstack-image.nix ]; }" -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let inherit (lib) mkOption types; copyChannel = true; @@ -12,7 +17,6 @@ in ../../../modules/virtualisation/openstack-config.nix ] ++ (lib.optional copyChannel ../../../modules/installer/cd-dvd/channel.nix); - options.openstackImage = { name = mkOption { type = types.str; @@ -33,7 +37,10 @@ in }; format = mkOption { - type = types.enum [ "raw" "qcow2" ]; + type = types.enum [ + "raw" + "qcow2" + ]; default = "qcow2"; description = "The image format to output"; }; @@ -59,13 +66,12 @@ in inherit (cfg) contents format name; pkgs = import ../../../.. { inherit (pkgs) system; }; # ensure we use the regular qemu-kvm package - configFile = pkgs.writeText "configuration.nix" - '' - { modulesPath, ... }: { - imports = [ "''${modulesPath}/virtualisation/openstack-config.nix" ]; - openstack.zfs.enable = true; - } - ''; + configFile = pkgs.writeText "configuration.nix" '' + { modulesPath, ... }: { + imports = [ "''${modulesPath}/virtualisation/openstack-config.nix" ]; + openstack.zfs.enable = true; + } + ''; includeChannel = copyChannel; diff --git a/nixos/modules/virtualisation/azure-image.nix b/nixos/modules/virtualisation/azure-image.nix index ecb57483cce9..1f6b2bd52c04 100644 --- a/nixos/modules/virtualisation/azure-image.nix +++ b/nixos/modules/virtualisation/azure-image.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; let @@ -35,7 +40,12 @@ in }; vmGeneration = mkOption { - type = with types; enum [ "v1" "v2" ]; + type = + with types; + enum [ + "v1" + "v2" + ]; default = "v1"; description = '' VM Generation to use. diff --git a/nixos/modules/virtualisation/digital-ocean-image.nix b/nixos/modules/virtualisation/digital-ocean-image.nix index 53791e911406..2d06c4c38fa1 100644 --- a/nixos/modules/virtualisation/digital-ocean-image.nix +++ b/nixos/modules/virtualisation/digital-ocean-image.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; let @@ -31,7 +36,10 @@ in }; virtualisation.digitalOceanImage.compressionMethod = mkOption { - type = types.enum [ "gzip" "bzip2" ]; + type = types.enum [ + "gzip" + "bzip2" + ]; default = "gzip"; example = "bzip2"; description = '' @@ -48,23 +56,32 @@ in system.build.digitalOceanImage = import ../../lib/make-disk-image.nix { name = "digital-ocean-image"; format = "qcow2"; - postVM = let - compress = { - "gzip" = "${pkgs.gzip}/bin/gzip"; - "bzip2" = "${pkgs.bzip2}/bin/bzip2"; - }.${cfg.compressionMethod}; - in '' - ${compress} $diskImage - ''; - configFile = if cfg.configFile == null - then config.virtualisation.digitalOcean.defaultConfigFile - else cfg.configFile; + postVM = + let + compress = + { + "gzip" = "${pkgs.gzip}/bin/gzip"; + "bzip2" = "${pkgs.bzip2}/bin/bzip2"; + } + .${cfg.compressionMethod}; + in + '' + ${compress} $diskImage + ''; + configFile = + if cfg.configFile == null then + config.virtualisation.digitalOcean.defaultConfigFile + else + cfg.configFile; inherit (cfg) diskSize; inherit config lib pkgs; }; }; - meta.maintainers = with maintainers; [ arianvp eamsden ]; + meta.maintainers = with maintainers; [ + arianvp + eamsden + ]; } diff --git a/nixos/modules/virtualisation/google-compute-image.nix b/nixos/modules/virtualisation/google-compute-image.nix index 8e7b31b439bf..416b47b768d9 100644 --- a/nixos/modules/virtualisation/google-compute-image.nix +++ b/nixos/modules/virtualisation/google-compute-image.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; let @@ -64,7 +69,13 @@ in system.build.googleComputeImage = import ../../lib/make-disk-image.nix { name = "google-compute-image"; postVM = '' - PATH=$PATH:${with pkgs; lib.makeBinPath [ gnutar gzip ]} + PATH=$PATH:${ + with pkgs; + lib.makeBinPath [ + gnutar + gzip + ] + } pushd $out mv $diskImage disk.raw tar -Sc disk.raw | gzip -${toString cfg.compressionLevel} > \ diff --git a/nixos/modules/virtualisation/hyperv-image.nix b/nixos/modules/virtualisation/hyperv-image.nix index eb1bbe9f3a58..d4ed256d0d91 100644 --- a/nixos/modules/virtualisation/hyperv-image.nix +++ b/nixos/modules/virtualisation/hyperv-image.nix @@ -1,11 +1,17 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with lib; let cfg = config.hyperv; -in { +in +{ options = { hyperv = { baseImageSize = mkOption { diff --git a/nixos/modules/virtualisation/linode-image.nix b/nixos/modules/virtualisation/linode-image.nix index 51f793ac011d..48d7f98e6abe 100644 --- a/nixos/modules/virtualisation/linode-image.nix +++ b/nixos/modules/virtualisation/linode-image.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; let diff --git a/nixos/modules/virtualisation/oci-image.nix b/nixos/modules/virtualisation/oci-image.nix index 1e2b90bfd46e..b867e7ae30e7 100644 --- a/nixos/modules/virtualisation/oci-image.nix +++ b/nixos/modules/virtualisation/oci-image.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.oci; @@ -25,7 +30,10 @@ in after = [ "network-online.target" ]; wants = [ "network-online.target" ]; - path = [ pkgs.coreutils pkgs.curl ]; + path = [ + pkgs.coreutils + pkgs.curl + ]; script = '' mkdir -m 0700 -p /root/.ssh if [ -f /root/.ssh/authorized_keys ]; then diff --git a/nixos/modules/virtualisation/oci-options.nix b/nixos/modules/virtualisation/oci-options.nix index 76f3475a4281..629b651ca5ac 100644 --- a/nixos/modules/virtualisation/oci-options.nix +++ b/nixos/modules/virtualisation/oci-options.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: { options = { oci = { diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix index d390c78432ae..4364fbff2a83 100644 --- a/nixos/modules/virtualisation/proxmox-image.nix +++ b/nixos/modules/virtualisation/proxmox-image.nix @@ -1,4 +1,9 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with lib; @@ -54,7 +59,10 @@ with lib; ''; }; bios = mkOption { - type = types.enum [ "seabios" "ovmf" ]; + type = types.enum [ + "seabios" + "ovmf" + ]; default = "seabios"; description = '' Select BIOS implementation (seabios = Legacy BIOS, ovmf = UEFI). @@ -124,8 +132,13 @@ with lib; }; }; qemuExtraConf = mkOption { - type = with types; attrsOf (oneOf [ str int ]); - default = {}; + type = + with types; + attrsOf (oneOf [ + str + int + ]); + default = { }; example = literalExpression '' { cpu = "host"; @@ -137,7 +150,12 @@ with lib; ''; }; partitionTableType = mkOption { - type = types.enum [ "efi" "hybrid" "legacy" "legacy+gpt" ]; + type = types.enum [ + "efi" + "hybrid" + "legacy" + "legacy+gpt" + ]; description = '' Partition table type to use. See make-disk-image.nix partitionTableType for details. Defaults to 'legacy' for 'proxmox.qemuConf.bios="seabios"' (default), other bios values defaults to 'efi'. @@ -185,142 +203,162 @@ with lib; }; }; - config = let - cfg = config.proxmox; - cfgLine = name: value: '' - ${name}: ${builtins.toString value} - ''; - virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0); - cfgFile = fileName: properties: pkgs.writeTextDir fileName '' - # generated by NixOS - ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)} - #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw: - ''; - inherit (cfg) partitionTableType; - supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid"; - supportBios = partitionTableType == "legacy" || partitionTableType == "hybrid" || partitionTableType == "legacy+gpt"; - hasBootPartition = partitionTableType == "efi" || partitionTableType == "hybrid"; - hasNoFsPartition = partitionTableType == "hybrid" || partitionTableType == "legacy+gpt"; - in { - assertions = [ - { - assertion = config.boot.loader.systemd-boot.enable -> config.proxmox.qemuConf.bios == "ovmf"; - message = "systemd-boot requires 'ovmf' bios"; - } - { - assertion = partitionTableType == "efi" -> config.proxmox.qemuConf.bios == "ovmf"; - message = "'efi' disk partitioning requires 'ovmf' bios"; - } - { - assertion = partitionTableType == "legacy" -> config.proxmox.qemuConf.bios == "seabios"; - message = "'legacy' disk partitioning requires 'seabios' bios"; - } - { - assertion = partitionTableType == "legacy+gpt" -> config.proxmox.qemuConf.bios == "seabios"; - message = "'legacy+gpt' disk partitioning requires 'seabios' bios"; - } - ]; - system.build.VMA = import ../../lib/make-disk-image.nix { - name = "proxmox-${cfg.filenameSuffix}"; - inherit (cfg) partitionTableType; - postVM = let - # Build qemu with PVE's patch that adds support for the VMA format - vma = (pkgs.qemu_kvm.override { - alsaSupport = false; - pulseSupport = false; - sdlSupport = false; - jackSupport = false; - gtkSupport = false; - vncSupport = false; - smartcardSupport = false; - spiceSupport = false; - ncursesSupport = false; - libiscsiSupport = false; - tpmSupport = false; - numaSupport = false; - seccompSupport = false; - guestAgentSupport = false; - }).overrideAttrs ( super: rec { - # Check https://github.com/proxmox/pve-qemu/tree/master for the version - # of qemu and patch to use - version = "9.0.0"; - src = pkgs.fetchurl { - url = "https://download.qemu.org/qemu-${version}.tar.xz"; - hash = "sha256-MnCKxmww2MiSYz6paMdxwcdtWX1w3erSGg0izPOG2mk="; - }; - patches = [ - # Proxmox' VMA tool is published as a particular patch upon QEMU - "${pkgs.fetchFromGitHub { - owner = "proxmox"; - repo = "pve-qemu"; - rev = "14afbdd55f04d250bd679ca1ad55d3f47cd9d4c8"; - hash = "sha256-lSJQA5SHIHfxJvMLIID2drv2H43crTPMNIlIT37w9Nc="; - }}/debian/patches/pve/0027-PVE-Backup-add-vma-backup-format-code.patch" - ]; - - buildInputs = super.buildInputs ++ [ pkgs.libuuid ]; - nativeBuildInputs = super.nativeBuildInputs ++ [ pkgs.perl ]; - - }); - in - '' - ${vma}/bin/vma create "vzdump-qemu-${cfg.filenameSuffix}.vma" \ - -c ${cfgFile "qemu-server.conf" (cfg.qemuConf // cfg.qemuExtraConf)}/qemu-server.conf drive-virtio0=$diskImage - rm $diskImage - ${pkgs.zstd}/bin/zstd "vzdump-qemu-${cfg.filenameSuffix}.vma" - mv "vzdump-qemu-${cfg.filenameSuffix}.vma.zst" $out/ - - mkdir -p $out/nix-support - echo "file vma $out/vzdump-qemu-${cfg.filenameSuffix}.vma.zst" > $out/nix-support/hydra-build-products + config = + let + cfg = config.proxmox; + cfgLine = name: value: '' + ${name}: ${builtins.toString value} ''; - inherit (cfg.qemuConf) additionalSpace diskSize bootSize; - format = "raw"; - inherit config lib pkgs; - }; + virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0); + cfgFile = + fileName: properties: + pkgs.writeTextDir fileName '' + # generated by NixOS + ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)} + #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw: + ''; + inherit (cfg) partitionTableType; + supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid"; + supportBios = + partitionTableType == "legacy" + || partitionTableType == "hybrid" + || partitionTableType == "legacy+gpt"; + hasBootPartition = partitionTableType == "efi" || partitionTableType == "hybrid"; + hasNoFsPartition = partitionTableType == "hybrid" || partitionTableType == "legacy+gpt"; + in + { + assertions = [ + { + assertion = config.boot.loader.systemd-boot.enable -> config.proxmox.qemuConf.bios == "ovmf"; + message = "systemd-boot requires 'ovmf' bios"; + } + { + assertion = partitionTableType == "efi" -> config.proxmox.qemuConf.bios == "ovmf"; + message = "'efi' disk partitioning requires 'ovmf' bios"; + } + { + assertion = partitionTableType == "legacy" -> config.proxmox.qemuConf.bios == "seabios"; + message = "'legacy' disk partitioning requires 'seabios' bios"; + } + { + assertion = partitionTableType == "legacy+gpt" -> config.proxmox.qemuConf.bios == "seabios"; + message = "'legacy+gpt' disk partitioning requires 'seabios' bios"; + } + ]; + system.build.VMA = import ../../lib/make-disk-image.nix { + name = "proxmox-${cfg.filenameSuffix}"; + inherit (cfg) partitionTableType; + postVM = + let + # Build qemu with PVE's patch that adds support for the VMA format + vma = + (pkgs.qemu_kvm.override { + alsaSupport = false; + pulseSupport = false; + sdlSupport = false; + jackSupport = false; + gtkSupport = false; + vncSupport = false; + smartcardSupport = false; + spiceSupport = false; + ncursesSupport = false; + libiscsiSupport = false; + tpmSupport = false; + numaSupport = false; + seccompSupport = false; + guestAgentSupport = false; + }).overrideAttrs + (super: rec { + # Check https://github.com/proxmox/pve-qemu/tree/master for the version + # of qemu and patch to use + version = "9.0.0"; + src = pkgs.fetchurl { + url = "https://download.qemu.org/qemu-${version}.tar.xz"; + hash = "sha256-MnCKxmww2MiSYz6paMdxwcdtWX1w3erSGg0izPOG2mk="; + }; + patches = [ + # Proxmox' VMA tool is published as a particular patch upon QEMU + "${ + pkgs.fetchFromGitHub { + owner = "proxmox"; + repo = "pve-qemu"; + rev = "14afbdd55f04d250bd679ca1ad55d3f47cd9d4c8"; + hash = "sha256-lSJQA5SHIHfxJvMLIID2drv2H43crTPMNIlIT37w9Nc="; + } + }/debian/patches/pve/0027-PVE-Backup-add-vma-backup-format-code.patch" + ]; - boot = { - growPartition = true; - kernelParams = [ "console=ttyS0" ]; - loader.grub = { - device = lib.mkDefault (if (hasNoFsPartition || supportBios) then - # Even if there is a separate no-fs partition ("/dev/disk/by-partlabel/no-fs" i.e. "/dev/vda2"), - # which will be used the bootloader, do not set it as loader.grub.device. - # GRUB installation fails, unless the whole disk is selected. - "/dev/vda" - else - "nodev"); - efiSupport = lib.mkDefault supportEfi; - efiInstallAsRemovable = lib.mkDefault supportEfi; + buildInputs = super.buildInputs ++ [ pkgs.libuuid ]; + nativeBuildInputs = super.nativeBuildInputs ++ [ pkgs.perl ]; + + }); + in + '' + ${vma}/bin/vma create "vzdump-qemu-${cfg.filenameSuffix}.vma" \ + -c ${ + cfgFile "qemu-server.conf" (cfg.qemuConf // cfg.qemuExtraConf) + }/qemu-server.conf drive-virtio0=$diskImage + rm $diskImage + ${pkgs.zstd}/bin/zstd "vzdump-qemu-${cfg.filenameSuffix}.vma" + mv "vzdump-qemu-${cfg.filenameSuffix}.vma.zst" $out/ + + mkdir -p $out/nix-support + echo "file vma $out/vzdump-qemu-${cfg.filenameSuffix}.vma.zst" > $out/nix-support/hydra-build-products + ''; + inherit (cfg.qemuConf) additionalSpace diskSize bootSize; + format = "raw"; + inherit config lib pkgs; }; - loader.timeout = 0; - initrd.availableKernelModules = [ "uas" "virtio_blk" "virtio_pci" ]; - }; + boot = { + growPartition = true; + kernelParams = [ "console=ttyS0" ]; + loader.grub = { + device = lib.mkDefault ( + if (hasNoFsPartition || supportBios) then + # Even if there is a separate no-fs partition ("/dev/disk/by-partlabel/no-fs" i.e. "/dev/vda2"), + # which will be used the bootloader, do not set it as loader.grub.device. + # GRUB installation fails, unless the whole disk is selected. + "/dev/vda" + else + "nodev" + ); + efiSupport = lib.mkDefault supportEfi; + efiInstallAsRemovable = lib.mkDefault supportEfi; + }; - fileSystems."/" = { - device = "/dev/disk/by-label/nixos"; - autoResize = true; - fsType = "ext4"; - }; - fileSystems."/boot" = lib.mkIf hasBootPartition { - device = "/dev/disk/by-label/ESP"; - fsType = "vfat"; - }; - - networking = mkIf cfg.cloudInit.enable { - hostName = mkForce ""; - useDHCP = false; - }; - - services = { - cloud-init = mkIf cfg.cloudInit.enable { - enable = true; - network.enable = true; + loader.timeout = 0; + initrd.availableKernelModules = [ + "uas" + "virtio_blk" + "virtio_pci" + ]; }; - sshd.enable = mkDefault true; - qemuGuest.enable = true; - }; - proxmox.qemuExtraConf.${cfg.cloudInit.device} = "${cfg.cloudInit.defaultStorage}:vm-9999-cloudinit,media=cdrom"; - }; + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + autoResize = true; + fsType = "ext4"; + }; + fileSystems."/boot" = lib.mkIf hasBootPartition { + device = "/dev/disk/by-label/ESP"; + fsType = "vfat"; + }; + + networking = mkIf cfg.cloudInit.enable { + hostName = mkForce ""; + useDHCP = false; + }; + + services = { + cloud-init = mkIf cfg.cloudInit.enable { + enable = true; + network.enable = true; + }; + sshd.enable = mkDefault true; + qemuGuest.enable = true; + }; + + proxmox.qemuExtraConf.${cfg.cloudInit.device} = "${cfg.cloudInit.defaultStorage}:vm-9999-cloudinit,media=cdrom"; + }; } diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index c6084e559096..fe803c1971ca 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -4,7 +4,13 @@ # `config'. By default, the Nix store is shared read-only with the # host, which makes (re)building VMs very efficient. -{ config, lib, pkgs, options, ... }: +{ + config, + lib, + pkgs, + options, + ... +}: with lib; @@ -22,236 +28,263 @@ let consoles = lib.concatMapStringsSep " " (c: "console=${c}") cfg.qemu.consoles; - driveOpts = { ... }: { + driveOpts = + { ... }: + { - options = { + options = { - file = mkOption { - type = types.str; - description = "The file image used for this drive."; - }; + file = mkOption { + type = types.str; + description = "The file image used for this drive."; + }; - driveExtraOpts = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Extra options passed to drive flag."; - }; + driveExtraOpts = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Extra options passed to drive flag."; + }; - deviceExtraOpts = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Extra options passed to device flag."; - }; + deviceExtraOpts = mkOption { + type = types.attrsOf types.str; + default = { }; + 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."; + }; - 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."; }; }; - }; + selectPartitionTableLayout = + { useEFIBoot, useDefaultFilesystems }: + if useDefaultFilesystems then if useEFIBoot then "efi" else "legacy" else "none"; - selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }: - if useDefaultFilesystems then - if useEFIBoot then "efi" else "legacy" - else "none"; - - driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }: + driveCmdline = + idx: + { + file, + driveExtraOpts, + deviceExtraOpts, + ... + }: let drvId = "drive${toString idx}"; - mkKeyValue = generators.mkKeyValueDefault {} "="; + mkKeyValue = generators.mkKeyValueDefault { } "="; mkOpts = opts: concatStringsSep "," (mapAttrsToList mkKeyValue opts); - driveOpts = mkOpts (driveExtraOpts // { - index = idx; - id = drvId; - "if" = "none"; - inherit file; - }); - deviceOpts = mkOpts (deviceExtraOpts // { - drive = drvId; - }); + driveOpts = mkOpts ( + driveExtraOpts + // { + index = idx; + id = drvId; + "if" = "none"; + inherit file; + } + ); + deviceOpts = mkOpts ( + deviceExtraOpts + // { + drive = drvId; + } + ); device = if cfg.qemu.diskInterface == "scsi" then "-device lsi53c895a -device scsi-hd,${deviceOpts}" else "-device virtio-blk-pci,${deviceOpts}"; in - "-drive ${driveOpts} ${device}"; + "-drive ${driveOpts} ${device}"; drivesCmdLine = drives: concatStringsSep "\\\n " (imap1 driveCmdline drives); # Shell script to start the VM. - startVM = - '' - #! ${hostPkgs.runtimeShell} + startVM = '' + #! ${hostPkgs.runtimeShell} - export PATH=${makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH + export PATH=${makeBinPath [ hostPkgs.coreutils ]}''${PATH:+:}$PATH - set -e + set -e - # Create an empty ext4 filesystem image. A filesystem image does not - # contain a partition table but just a filesystem. - createEmptyFilesystemImage() { - local name=$1 - local size=$2 - local temp=$(mktemp) - ${qemu}/bin/qemu-img create -f raw "$temp" "$size" - ${hostPkgs.e2fsprogs}/bin/mkfs.ext4 -L ${rootFilesystemLabel} "$temp" - ${qemu}/bin/qemu-img convert -f raw -O qcow2 "$temp" "$name" - rm "$temp" - } + # Create an empty ext4 filesystem image. A filesystem image does not + # contain a partition table but just a filesystem. + createEmptyFilesystemImage() { + local name=$1 + local size=$2 + local temp=$(mktemp) + ${qemu}/bin/qemu-img create -f raw "$temp" "$size" + ${hostPkgs.e2fsprogs}/bin/mkfs.ext4 -L ${rootFilesystemLabel} "$temp" + ${qemu}/bin/qemu-img convert -f raw -O qcow2 "$temp" "$name" + rm "$temp" + } - NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE" + NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE" - if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then - echo "Disk image do not exist, creating the virtualisation disk image..." + if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then + echo "Disk image do not exist, creating the virtualisation disk image..." - ${if (cfg.useBootLoader && cfg.useDefaultFilesystems) then '' - # Create a writable qcow2 image using the systemImage as a backing - # image. + ${ + if (cfg.useBootLoader && cfg.useDefaultFilesystems) then + '' + # Create a writable qcow2 image using the systemImage as a backing + # image. - # CoW prevent size to be attributed to an image. - # FIXME: raise this issue to upstream. - ${qemu}/bin/qemu-img create \ - -f qcow2 \ - -b ${systemImage}/nixos.qcow2 \ - -F qcow2 \ - "$NIX_DISK_IMAGE" - '' else if cfg.useDefaultFilesystems then '' - createEmptyFilesystemImage "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M" - '' else '' - # Create an empty disk image without a filesystem. - ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M" - '' - } - echo "Virtualisation disk image created." - fi - - # Create a directory for storing temporary data of the running VM. - if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then - TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir) - fi - - ${lib.optionalString (cfg.useNixStoreImage) '' - echo "Creating Nix store image..." - - ${hostPkgs.gnutar}/bin/tar --create \ - --absolute-names \ - --verbatim-files-from \ - --transform 'flags=rSh;s|/nix/store/||' \ - --transform 'flags=rSh;s|~nix~case~hack~[[:digit:]]\+||g' \ - --files-from ${hostPkgs.closureInfo { rootPaths = [ config.system.build.toplevel regInfo ]; }}/store-paths \ - | ${hostPkgs.erofs-utils}/bin/mkfs.erofs \ - --quiet \ - --force-uid=0 \ - --force-gid=0 \ - -L ${nixStoreFilesystemLabel} \ - -U eb176051-bd15-49b7-9e6b-462e0b467019 \ - -T 0 \ - --tar=f \ - "$TMPDIR"/store.img - - echo "Created Nix store image." - '' - } - - # Create a directory for exchanging data with the VM. - mkdir -p "$TMPDIR/xchg" - - ${lib.optionalString cfg.useHostCerts - '' - mkdir -p "$TMPDIR/certs" - if [ -e "$NIX_SSL_CERT_FILE" ]; then - cp -L "$NIX_SSL_CERT_FILE" "$TMPDIR"/certs/ca-certificates.crt - else - echo \$NIX_SSL_CERT_FILE should point to a valid file if virtualisation.useHostCerts is enabled. - fi - ''} - - ${lib.optionalString cfg.useEFIBoot - '' - # Expose EFI variables, it's useful even when we are not using a bootloader (!). - # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence - # no guard against `useBootLoader`. Examples: - # - testing PXE boot or other EFI applications - # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows - NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}") - # VM needs writable EFI vars - if ! test -e "$NIX_EFI_VARS"; then - ${if cfg.efi.keepVariables then - # We still need the EFI var from the make-disk-image derivation - # because our "switch-to-configuration" process might - # write into it and we want to keep this data. - ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"'' - else - ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"'' - } - chmod 0644 "$NIX_EFI_VARS" - fi - ''} - - ${lib.optionalString cfg.tpm.enable '' - NIX_SWTPM_DIR=$(readlink -f "''${NIX_SWTPM_DIR:-${config.system.name}-swtpm}") - mkdir -p "$NIX_SWTPM_DIR" - ${lib.getExe cfg.tpm.package} \ - socket \ - --tpmstate dir="$NIX_SWTPM_DIR" \ - --ctrl type=unixio,path="$NIX_SWTPM_DIR"/socket,terminate \ - --pid file="$NIX_SWTPM_DIR"/pid --daemon \ - --tpm2 \ - --log file="$NIX_SWTPM_DIR"/stdout,level=6 - - # Enable `fdflags` builtin in Bash - # We will need it to perform surgical modification of the file descriptor - # passed in the coprocess to remove `FD_CLOEXEC`, i.e. close the file descriptor - # on exec. - # If let alone, it will trigger the coprocess to read EOF when QEMU is `exec` - # at the end of this script. To work around that, we will just clear - # the `FD_CLOEXEC` bits as a first step. - enable -f ${hostPkgs.bash}/lib/bash/fdflags fdflags - # leave a dangling subprocess because the swtpm ctrl socket has - # "terminate" when the last connection disconnects, it stops swtpm. - # When qemu stops, or if the main shell process ends, the coproc will - # get signaled by virtue of the pipe between main and coproc ending. - # Which in turns triggers a socat connect-disconnect to swtpm which - # will stop it. - coproc waitingswtpm { - read || : - echo "" | ${lib.getExe hostPkgs.socat} STDIO UNIX-CONNECT:"$NIX_SWTPM_DIR"/socket + # CoW prevent size to be attributed to an image. + # FIXME: raise this issue to upstream. + ${qemu}/bin/qemu-img create \ + -f qcow2 \ + -b ${systemImage}/nixos.qcow2 \ + -F qcow2 \ + "$NIX_DISK_IMAGE" + '' + else if cfg.useDefaultFilesystems then + '' + createEmptyFilesystemImage "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M" + '' + else + '' + # Create an empty disk image without a filesystem. + ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" "${toString cfg.diskSize}M" + '' } - # Clear `FD_CLOEXEC` on the coprocess' file descriptor stdin. - fdflags -s-cloexec ''${waitingswtpm[1]} - ''} + echo "Virtualisation disk image created." + fi - cd "$TMPDIR" + # Create a directory for storing temporary data of the running VM. + if [ -z "$TMPDIR" ] || [ -z "$USE_TMPDIR" ]; then + TMPDIR=$(mktemp -d nix-vm.XXXXXXXXXX --tmpdir) + fi - ${lib.optionalString (cfg.emptyDiskImages != []) "idx=0"} - ${flip concatMapStrings cfg.emptyDiskImages (size: '' - if ! test -e "empty$idx.qcow2"; then - ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M" - fi - idx=$((idx + 1)) - '')} + ${lib.optionalString (cfg.useNixStoreImage) '' + echo "Creating Nix store image..." - # Start QEMU. - exec ${qemu-common.qemuBinary qemu} \ - -name ${config.system.name} \ - -m ${toString config.virtualisation.memorySize} \ - -smp ${toString config.virtualisation.cores} \ - -device virtio-rng-pci \ - ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \ - ${concatStringsSep " \\\n " - (mapAttrsToList - (tag: share: "-virtfs local,path=${share.source},security_model=${share.securityModel},mount_tag=${tag}") - config.virtualisation.sharedDirectories)} \ - ${drivesCmdLine config.virtualisation.qemu.drives} \ - ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \ - $QEMU_OPTS \ - "$@" - ''; + ${hostPkgs.gnutar}/bin/tar --create \ + --absolute-names \ + --verbatim-files-from \ + --transform 'flags=rSh;s|/nix/store/||' \ + --transform 'flags=rSh;s|~nix~case~hack~[[:digit:]]\+||g' \ + --files-from ${ + hostPkgs.closureInfo { + rootPaths = [ + config.system.build.toplevel + regInfo + ]; + } + }/store-paths \ + | ${hostPkgs.erofs-utils}/bin/mkfs.erofs \ + --quiet \ + --force-uid=0 \ + --force-gid=0 \ + -L ${nixStoreFilesystemLabel} \ + -U eb176051-bd15-49b7-9e6b-462e0b467019 \ + -T 0 \ + --tar=f \ + "$TMPDIR"/store.img + echo "Created Nix store image." + ''} + + # Create a directory for exchanging data with the VM. + mkdir -p "$TMPDIR/xchg" + + ${lib.optionalString cfg.useHostCerts '' + mkdir -p "$TMPDIR/certs" + if [ -e "$NIX_SSL_CERT_FILE" ]; then + cp -L "$NIX_SSL_CERT_FILE" "$TMPDIR"/certs/ca-certificates.crt + else + echo \$NIX_SSL_CERT_FILE should point to a valid file if virtualisation.useHostCerts is enabled. + fi + ''} + + ${lib.optionalString cfg.useEFIBoot '' + # Expose EFI variables, it's useful even when we are not using a bootloader (!). + # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence + # no guard against `useBootLoader`. Examples: + # - testing PXE boot or other EFI applications + # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows + NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}") + # VM needs writable EFI vars + if ! test -e "$NIX_EFI_VARS"; then + ${ + if cfg.efi.keepVariables then + # We still need the EFI var from the make-disk-image derivation + # because our "switch-to-configuration" process might + # write into it and we want to keep this data. + ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"'' + else + ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"'' + } + chmod 0644 "$NIX_EFI_VARS" + fi + ''} + + ${lib.optionalString cfg.tpm.enable '' + NIX_SWTPM_DIR=$(readlink -f "''${NIX_SWTPM_DIR:-${config.system.name}-swtpm}") + mkdir -p "$NIX_SWTPM_DIR" + ${lib.getExe cfg.tpm.package} \ + socket \ + --tpmstate dir="$NIX_SWTPM_DIR" \ + --ctrl type=unixio,path="$NIX_SWTPM_DIR"/socket,terminate \ + --pid file="$NIX_SWTPM_DIR"/pid --daemon \ + --tpm2 \ + --log file="$NIX_SWTPM_DIR"/stdout,level=6 + + # Enable `fdflags` builtin in Bash + # We will need it to perform surgical modification of the file descriptor + # passed in the coprocess to remove `FD_CLOEXEC`, i.e. close the file descriptor + # on exec. + # If let alone, it will trigger the coprocess to read EOF when QEMU is `exec` + # at the end of this script. To work around that, we will just clear + # the `FD_CLOEXEC` bits as a first step. + enable -f ${hostPkgs.bash}/lib/bash/fdflags fdflags + # leave a dangling subprocess because the swtpm ctrl socket has + # "terminate" when the last connection disconnects, it stops swtpm. + # When qemu stops, or if the main shell process ends, the coproc will + # get signaled by virtue of the pipe between main and coproc ending. + # Which in turns triggers a socat connect-disconnect to swtpm which + # will stop it. + coproc waitingswtpm { + read || : + echo "" | ${lib.getExe hostPkgs.socat} STDIO UNIX-CONNECT:"$NIX_SWTPM_DIR"/socket + } + # Clear `FD_CLOEXEC` on the coprocess' file descriptor stdin. + fdflags -s-cloexec ''${waitingswtpm[1]} + ''} + + cd "$TMPDIR" + + ${lib.optionalString (cfg.emptyDiskImages != [ ]) "idx=0"} + ${flip concatMapStrings cfg.emptyDiskImages (size: '' + if ! test -e "empty$idx.qcow2"; then + ${qemu}/bin/qemu-img create -f qcow2 "empty$idx.qcow2" "${toString size}M" + fi + idx=$((idx + 1)) + '')} + + # Start QEMU. + exec ${qemu-common.qemuBinary qemu} \ + -name ${config.system.name} \ + -m ${toString config.virtualisation.memorySize} \ + -smp ${toString config.virtualisation.cores} \ + -device virtio-rng-pci \ + ${concatStringsSep " " config.virtualisation.qemu.networkingOptions} \ + ${ + concatStringsSep " \\\n " ( + mapAttrsToList ( + tag: share: + "-virtfs local,path=${share.source},security_model=${share.securityModel},mount_tag=${tag}" + ) config.virtualisation.sharedDirectories + ) + } \ + ${drivesCmdLine config.virtualisation.qemu.drives} \ + ${concatStringsSep " \\\n " config.virtualisation.qemu.options} \ + $QEMU_OPTS \ + "$@" + ''; regInfo = hostPkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; }; @@ -290,210 +323,242 @@ in { imports = [ ../profiles/qemu-guest.nix - (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ]) - (mkRemovedOptionModule [ "virtualisation" "bootDevice" ] "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues.") - (mkRemovedOptionModule [ "virtualisation" "efiVars" ] "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue") - (mkRemovedOptionModule [ "virtualisation" "persistBootDevice" ] "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`") + (mkRenamedOptionModule + [ + "virtualisation" + "pathsInNixDB" + ] + [ + "virtualisation" + "additionalPaths" + ] + ) + (mkRemovedOptionModule + [ + "virtualisation" + "bootDevice" + ] + "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues." + ) + (mkRemovedOptionModule + [ + "virtualisation" + "efiVars" + ] + "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue" + ) + (mkRemovedOptionModule + [ + "virtualisation" + "persistBootDevice" + ] + "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`" + ) ]; options = { virtualisation.fileSystems = options.fileSystems; - virtualisation.memorySize = - mkOption { - type = types.ints.positive; - default = 1024; - description = '' - The memory size in megabytes of the virtual machine. - ''; + virtualisation.memorySize = mkOption { + type = types.ints.positive; + default = 1024; + description = '' + The memory size in megabytes of the virtual machine. + ''; + }; + + virtualisation.msize = mkOption { + type = types.ints.positive; + default = 16384; + description = '' + The msize (maximum packet size) option passed to 9p file systems, in + bytes. Increasing this should increase performance significantly, + at the cost of higher RAM usage. + ''; + }; + + virtualisation.diskSize = mkOption { + type = types.ints.positive; + default = 1024; + description = '' + The disk size in megabytes of the virtual machine. + ''; + }; + + virtualisation.diskImage = mkOption { + type = types.nullOr types.str; + default = "./${config.system.name}.qcow2"; + defaultText = literalExpression ''"./''${config.system.name}.qcow2"''; + description = '' + Path to the disk image containing the root filesystem. + The image will be created on startup if it does not + exist. + + If null, a tmpfs will be used as the root filesystem and + the VM's state will not be persistent. + ''; + }; + + virtualisation.bootLoaderDevice = mkOption { + type = types.path; + default = "/dev/disk/by-id/virtio-${rootDriveSerialAttr}"; + defaultText = literalExpression ''/dev/disk/by-id/virtio-${rootDriveSerialAttr}''; + example = "/dev/disk/by-id/virtio-boot-loader-device"; + description = '' + The path (inside th VM) to the device to boot from when legacy booting. + ''; + }; + + virtualisation.bootPartition = mkOption { + type = types.nullOr types.path; + default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null; + defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null''; + example = "/dev/disk/by-label/esp"; + description = '' + The path (inside the VM) to the device containing the EFI System Partition (ESP). + + If you are *not* booting from a UEFI firmware, this value is, by + default, `null`. The ESP is mounted to `boot.loader.efi.efiSysMountpoint`. + ''; + }; + + virtualisation.rootDevice = mkOption { + type = types.nullOr types.path; + default = "/dev/disk/by-label/${rootFilesystemLabel}"; + defaultText = literalExpression ''/dev/disk/by-label/${rootFilesystemLabel}''; + example = "/dev/disk/by-label/nixos"; + description = '' + The path (inside the VM) to the device containing the root filesystem. + ''; + }; + + virtualisation.emptyDiskImages = mkOption { + type = types.listOf types.ints.positive; + default = [ ]; + description = '' + Additional disk images to provide to the VM. The value is + a list of size in megabytes of each disk. These disks are + writeable by the VM. + ''; + }; + + virtualisation.graphics = mkOption { + type = types.bool; + default = true; + description = '' + Whether to run QEMU with a graphics window, or in nographic mode. + Serial console will be enabled on both settings, but this will + change the preferred console. + ''; + }; + + virtualisation.resolution = mkOption { + type = options.services.xserver.resolutions.type.nestedTypes.elemType; + default = { + x = 1024; + y = 768; }; + description = '' + The resolution of the virtual machine display. + ''; + }; - virtualisation.msize = - mkOption { - type = types.ints.positive; - default = 16384; - description = '' - The msize (maximum packet size) option passed to 9p file systems, in - bytes. Increasing this should increase performance significantly, - at the cost of higher RAM usage. - ''; - }; + virtualisation.cores = mkOption { + type = types.ints.positive; + default = 1; + description = '' + Specify the number of cores the guest is permitted to use. + The number can be higher than the available cores on the + host system. + ''; + }; - virtualisation.diskSize = - mkOption { - type = types.ints.positive; - default = 1024; - description = '' - The disk size in megabytes of the virtual machine. - ''; - }; + virtualisation.sharedDirectories = mkOption { + type = types.attrsOf ( + types.submodule { + options.source = mkOption { + type = types.str; + description = "The path of the directory to share, can be a shell variable"; + }; + options.target = mkOption { + type = types.path; + description = "The mount point of the directory inside the virtual machine"; + }; + options.securityModel = mkOption { + type = types.enum [ + "passthrough" + "mapped-xattr" + "mapped-file" + "none" + ]; + default = "mapped-xattr"; + description = '' + The security model to use for this share: - virtualisation.diskImage = - mkOption { - type = types.nullOr types.str; - default = "./${config.system.name}.qcow2"; - defaultText = literalExpression ''"./''${config.system.name}.qcow2"''; - description = '' - Path to the disk image containing the root filesystem. - The image will be created on startup if it does not - exist. - - If null, a tmpfs will be used as the root filesystem and - the VM's state will not be persistent. - ''; - }; - - virtualisation.bootLoaderDevice = - mkOption { - type = types.path; - default = "/dev/disk/by-id/virtio-${rootDriveSerialAttr}"; - defaultText = literalExpression ''/dev/disk/by-id/virtio-${rootDriveSerialAttr}''; - example = "/dev/disk/by-id/virtio-boot-loader-device"; - description = '' - The path (inside th VM) to the device to boot from when legacy booting. - ''; - }; - - virtualisation.bootPartition = - mkOption { - type = types.nullOr types.path; - default = if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null; - defaultText = literalExpression ''if cfg.useEFIBoot then "/dev/disk/by-label/${espFilesystemLabel}" else null''; - example = "/dev/disk/by-label/esp"; - description = '' - The path (inside the VM) to the device containing the EFI System Partition (ESP). - - If you are *not* booting from a UEFI firmware, this value is, by - default, `null`. The ESP is mounted to `boot.loader.efi.efiSysMountpoint`. - ''; - }; - - virtualisation.rootDevice = - mkOption { - type = types.nullOr types.path; - default = "/dev/disk/by-label/${rootFilesystemLabel}"; - defaultText = literalExpression ''/dev/disk/by-label/${rootFilesystemLabel}''; - example = "/dev/disk/by-label/nixos"; - description = '' - The path (inside the VM) to the device containing the root filesystem. - ''; - }; - - virtualisation.emptyDiskImages = - mkOption { - type = types.listOf types.ints.positive; - default = []; - description = '' - Additional disk images to provide to the VM. The value is - a list of size in megabytes of each disk. These disks are - writeable by the VM. - ''; - }; - - virtualisation.graphics = - mkOption { - type = types.bool; - default = true; - description = '' - Whether to run QEMU with a graphics window, or in nographic mode. - Serial console will be enabled on both settings, but this will - change the preferred console. + - `passthrough`: files are stored using the same credentials as they are created on the guest (this requires QEMU to run as root) + - `mapped-xattr`: some of the file attributes like uid, gid, mode bits and link target are stored as file attributes + - `mapped-file`: the attributes are stored in the hidden .virtfs_metadata directory. Directories exported by this security model cannot interact with other unix tools + - `none`: same as "passthrough" except the sever won't report failures if it fails to set file attributes like ownership ''; - }; - - virtualisation.resolution = - mkOption { - type = options.services.xserver.resolutions.type.nestedTypes.elemType; - default = { x = 1024; y = 768; }; - description = '' - The resolution of the virtual machine display. - ''; - }; - - virtualisation.cores = - mkOption { - type = types.ints.positive; - default = 1; - description = '' - Specify the number of cores the guest is permitted to use. - The number can be higher than the available cores on the - host system. - ''; - }; - - virtualisation.sharedDirectories = - mkOption { - type = types.attrsOf - (types.submodule { - options.source = mkOption { - type = types.str; - description = "The path of the directory to share, can be a shell variable"; - }; - options.target = mkOption { - type = types.path; - description = "The mount point of the directory inside the virtual machine"; - }; - options.securityModel = mkOption { - type = types.enum [ "passthrough" "mapped-xattr" "mapped-file" "none" ]; - default = "mapped-xattr"; - description = '' - The security model to use for this share: - - - `passthrough`: files are stored using the same credentials as they are created on the guest (this requires QEMU to run as root) - - `mapped-xattr`: some of the file attributes like uid, gid, mode bits and link target are stored as file attributes - - `mapped-file`: the attributes are stored in the hidden .virtfs_metadata directory. Directories exported by this security model cannot interact with other unix tools - - `none`: same as "passthrough" except the sever won't report failures if it fails to set file attributes like ownership - ''; - }; - }); - default = { }; - example = { - my-share = { source = "/path/to/be/shared"; target = "/mnt/shared"; }; + }; + } + ); + default = { }; + example = { + my-share = { + source = "/path/to/be/shared"; + target = "/mnt/shared"; }; - description = '' - An attributes set of directories that will be shared with the - virtual machine using VirtFS (9P filesystem over VirtIO). - The attribute name will be used as the 9P mount tag. - ''; }; + description = '' + An attributes set of directories that will be shared with the + virtual machine using VirtFS (9P filesystem over VirtIO). + The attribute name will be used as the 9P mount tag. + ''; + }; - virtualisation.additionalPaths = - mkOption { - type = types.listOf types.path; - default = []; - description = '' - A list of paths whose closure should be made available to - the VM. + virtualisation.additionalPaths = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + A list of paths whose closure should be made available to + the VM. - When 9p is used, the closure is registered in the Nix - database in the VM. All other paths in the host Nix store - appear in the guest Nix store as well, but are considered - garbage (because they are not registered in the Nix - database of the guest). + When 9p is used, the closure is registered in the Nix + database in the VM. All other paths in the host Nix store + appear in the guest Nix store as well, but are considered + garbage (because they are not registered in the Nix + database of the guest). - When {option}`virtualisation.useNixStoreImage` is - set, the closure is copied to the Nix store image. - ''; - }; + When {option}`virtualisation.useNixStoreImage` is + set, the closure is copied to the Nix store image. + ''; + }; virtualisation.forwardPorts = mkOption { - type = types.listOf - (types.submodule { + type = types.listOf ( + types.submodule { options.from = mkOption { - type = types.enum [ "host" "guest" ]; + type = types.enum [ + "host" + "guest" + ]; default = "host"; description = '' - Controls the direction in which the ports are mapped: + Controls the direction in which the ports are mapped: - - `"host"` means traffic from the host ports - is forwarded to the given guest port. - - `"guest"` means traffic from the guest ports - is forwarded to the given host port. - ''; + - `"host"` means traffic from the host ports + is forwarded to the given guest port. + - `"guest"` means traffic from the guest ports + is forwarded to the given host port. + ''; }; options.proto = mkOption { - type = types.enum [ "tcp" "udp" ]; + type = types.enum [ + "tcp" + "udp" + ]; default = "tcp"; description = "The protocol to forward."; }; @@ -515,10 +580,10 @@ in type = types.port; description = "The guest port to be mapped."; }; - }); - default = []; - example = lib.literalExpression - '' + } + ); + default = [ ]; + example = lib.literalExpression '' [ # forward local port 2222 -> 22, to ssh into the VM { from = "host"; host.port = 2222; guest.port = 22; } @@ -528,122 +593,121 @@ in host.address = "127.0.0.1"; host.port = 80; } ] - ''; + ''; description = '' - When using the SLiRP user networking (default), this option allows to - forward ports to/from the host/guest. + When using the SLiRP user networking (default), this option allows to + forward ports to/from the host/guest. - ::: {.warning} - If the NixOS firewall on the virtual machine is enabled, you also - have to open the guest ports to enable the traffic between host and - guest. - ::: + ::: {.warning} + If the NixOS firewall on the virtual machine is enabled, you also + have to open the guest ports to enable the traffic between host and + guest. + ::: - ::: {.note} - Currently QEMU supports only IPv4 forwarding. - ::: - ''; + ::: {.note} + Currently QEMU supports only IPv4 forwarding. + ::: + ''; }; - virtualisation.restrictNetwork = - mkOption { - type = types.bool; - default = false; - example = true; - description = '' - If this option is enabled, the guest will be isolated, i.e. it will - not be able to contact the host and no guest IP packets will be - routed over the host to the outside. This option does not affect - any explicitly set forwarding rules. - ''; - }; + virtualisation.restrictNetwork = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + If this option is enabled, the guest will be isolated, i.e. it will + not be able to contact the host and no guest IP packets will be + routed over the host to the outside. This option does not affect + any explicitly set forwarding rules. + ''; + }; - virtualisation.vlans = - mkOption { - type = types.listOf types.ints.unsigned; - default = if config.virtualisation.interfaces == {} then [ 1 ] else [ ]; - defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]''; - example = [ 1 2 ]; - description = '' - Virtual networks to which the VM is connected. Each - number «N» in this list causes - the VM to have a virtual Ethernet interface attached to a - separate virtual network on which it will be assigned IP - address - `192.168.«N».«M»`, - where «M» is the index of this VM - in the list of VMs. - ''; - }; + virtualisation.vlans = mkOption { + type = types.listOf types.ints.unsigned; + default = if config.virtualisation.interfaces == { } then [ 1 ] else [ ]; + defaultText = lib.literalExpression ''if config.virtualisation.interfaces == {} then [ 1 ] else [ ]''; + example = [ + 1 + 2 + ]; + description = '' + Virtual networks to which the VM is connected. Each + number «N» in this list causes + the VM to have a virtual Ethernet interface attached to a + separate virtual network on which it will be assigned IP + address + `192.168.«N».«M»`, + where «M» is the index of this VM + in the list of VMs. + ''; + }; virtualisation.interfaces = mkOption { - default = {}; + default = { }; example = { enp1s0.vlan = 1; }; description = '' Network interfaces to add to the VM. ''; - type = with types; attrsOf (submodule { - options = { - vlan = mkOption { - type = types.ints.unsigned; - description = '' - VLAN to which the network interface is connected. - ''; - }; + type = + with types; + attrsOf (submodule { + options = { + vlan = mkOption { + type = types.ints.unsigned; + description = '' + VLAN to which the network interface is connected. + ''; + }; - assignIP = mkOption { - type = types.bool; - default = false; - description = '' - Automatically assign an IP address to the network interface using the same scheme as - virtualisation.vlans. - ''; + assignIP = mkOption { + type = types.bool; + default = false; + description = '' + Automatically assign an IP address to the network interface using the same scheme as + virtualisation.vlans. + ''; + }; }; - }; - }); + }); }; - virtualisation.writableStore = - mkOption { - type = types.bool; - default = cfg.mountHostNixStore; - defaultText = literalExpression "cfg.mountHostNixStore"; - description = '' - If enabled, the Nix store in the VM is made writable by - layering an overlay filesystem on top of the host's Nix - store. + virtualisation.writableStore = mkOption { + type = types.bool; + default = cfg.mountHostNixStore; + defaultText = literalExpression "cfg.mountHostNixStore"; + description = '' + If enabled, the Nix store in the VM is made writable by + layering an overlay filesystem on top of the host's Nix + store. - By default, this is enabled if you mount a host Nix store. - ''; - }; + By default, this is enabled if you mount a host Nix store. + ''; + }; - virtualisation.writableStoreUseTmpfs = - mkOption { - type = types.bool; - default = true; - description = '' - Use a tmpfs for the writable store instead of writing to the VM's - own filesystem. - ''; - }; + virtualisation.writableStoreUseTmpfs = mkOption { + type = types.bool; + default = true; + description = '' + Use a tmpfs for the writable store instead of writing to the VM's + own filesystem. + ''; + }; - networking.primaryIPAddress = - mkOption { - type = types.str; - default = ""; - internal = true; - description = "Primary IP address used in /etc/hosts."; - }; + networking.primaryIPAddress = mkOption { + type = types.str; + default = ""; + internal = true; + description = "Primary IP address used in /etc/hosts."; + }; - networking.primaryIPv6Address = - mkOption { - type = types.str; - default = ""; - internal = true; - description = "Primary IPv6 address used in /etc/hosts."; - }; + networking.primaryIPv6Address = mkOption { + type = types.str; + default = ""; + internal = true; + description = "Primary IPv6 address used in /etc/hosts."; + }; virtualisation.host.pkgs = mkOption { type = options.nixpkgs.pkgs.type; @@ -659,31 +723,38 @@ in }; virtualisation.qemu = { - package = - mkOption { - type = types.package; - default = if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then hostPkgs.qemu_kvm else hostPkgs.qemu; - defaultText = literalExpression "if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then config.virtualisation.host.pkgs.qemu_kvm else config.virtualisation.host.pkgs.qemu"; - example = literalExpression "pkgs.qemu_test"; - description = "QEMU package to use."; - }; + package = mkOption { + type = types.package; + default = + if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then + hostPkgs.qemu_kvm + else + hostPkgs.qemu; + defaultText = literalExpression "if hostPkgs.stdenv.hostPlatform.qemuArch == pkgs.stdenv.hostPlatform.qemuArch then config.virtualisation.host.pkgs.qemu_kvm else config.virtualisation.host.pkgs.qemu"; + example = literalExpression "pkgs.qemu_test"; + description = "QEMU package to use."; + }; - options = - mkOption { - type = types.listOf types.str; - default = []; - example = [ "-vga std" ]; - description = '' - Options passed to QEMU. - See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list. - ''; - }; + options = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "-vga std" ]; + description = '' + Options passed to QEMU. + See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list. + ''; + }; consoles = mkOption { type = types.listOf types.str; - default = let - consoles = [ "${qemu-common.qemuSerialDevice},115200n8" "tty0" ]; - in if cfg.graphics then consoles else reverseList consoles; + default = + let + consoles = [ + "${qemu-common.qemuSerialDevice},115200n8" + "tty0" + ]; + in + if cfg.graphics then consoles else reverseList consoles; example = [ "console=tty1" ]; description = '' The output console devices to pass to the kernel command line via the @@ -696,176 +767,170 @@ in ''; }; - networkingOptions = - mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ - "-net nic,netdev=user.0,model=virtio" - "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" - ]; - description = '' - Networking-related command-line options that should be passed to qemu. - The default is to use userspace networking (SLiRP). - See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details. + networkingOptions = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "-net nic,netdev=user.0,model=virtio" + "-netdev user,id=user.0,\${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}" + ]; + description = '' + Networking-related command-line options that should be passed to qemu. + The default is to use userspace networking (SLiRP). + See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details. - If you override this option, be advised to keep - `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example) - to keep the default runtime behaviour. - ''; - }; + If you override this option, be advised to keep + `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example) + to keep the default runtime behaviour. + ''; + }; - drives = - mkOption { - type = types.listOf (types.submodule driveOpts); - description = "Drives passed to qemu."; - }; + drives = mkOption { + type = types.listOf (types.submodule driveOpts); + description = "Drives passed to qemu."; + }; - diskInterface = - mkOption { - type = types.enum [ "virtio" "scsi" "ide" ]; - default = "virtio"; - example = "scsi"; - description = "The interface used for the virtual hard disks."; - }; + diskInterface = mkOption { + type = types.enum [ + "virtio" + "scsi" + "ide" + ]; + default = "virtio"; + example = "scsi"; + description = "The interface used for the virtual hard disks."; + }; - guestAgent.enable = - mkOption { - type = types.bool; - default = true; - description = '' - Enable the Qemu guest agent. - ''; - }; + guestAgent.enable = mkOption { + type = types.bool; + default = true; + description = '' + Enable the Qemu guest agent. + ''; + }; - virtioKeyboard = - mkOption { - type = types.bool; - default = true; - description = '' - Enable the virtio-keyboard device. - ''; - }; + virtioKeyboard = mkOption { + type = types.bool; + default = true; + description = '' + Enable the virtio-keyboard device. + ''; + }; }; - virtualisation.useNixStoreImage = - mkOption { - type = types.bool; - default = false; - description = '' - Build and use a disk image for the Nix store, instead of - accessing the host's one through 9p. + virtualisation.useNixStoreImage = mkOption { + type = types.bool; + default = false; + description = '' + Build and use a disk image for the Nix store, instead of + accessing the host's one through 9p. - For applications which do a lot of reads from the store, - this can drastically improve performance, but at the cost of - disk space and image build time. + For applications which do a lot of reads from the store, + this can drastically improve performance, but at the cost of + disk space and image build time. - The Nix store image is built just-in-time right before the VM is - started. Because it does not produce another derivation, the image is - not cached between invocations and never lands in the store or binary - cache. + The Nix store image is built just-in-time right before the VM is + started. Because it does not produce another derivation, the image is + not cached between invocations and never lands in the store or binary + cache. - If you want a full disk image with a partition table and a root - filesystem instead of only a store image, enable - {option}`virtualisation.useBootLoader` instead. - ''; - }; + If you want a full disk image with a partition table and a root + filesystem instead of only a store image, enable + {option}`virtualisation.useBootLoader` instead. + ''; + }; - virtualisation.mountHostNixStore = - mkOption { - type = types.bool; - default = !cfg.useNixStoreImage && !cfg.useBootLoader; - defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader"; - description = '' - Mount the host Nix store as a 9p mount. - ''; - }; + virtualisation.mountHostNixStore = mkOption { + type = types.bool; + default = !cfg.useNixStoreImage && !cfg.useBootLoader; + defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader"; + description = '' + Mount the host Nix store as a 9p mount. + ''; + }; virtualisation.directBoot = { - enable = - mkOption { - type = types.bool; - default = !cfg.useBootLoader; - defaultText = "!cfg.useBootLoader"; - description = '' - If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. - Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html) - - This is enabled by default. - If you want to test netboot, consider disabling this option. - Enable a bootloader with {option}`virtualisation.useBootLoader` if you need. - - Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU. - Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`. - They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details - For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`. - - This will not (re-)boot correctly into a system that has switched to a different configuration on disk. - ''; - }; - initrd = - mkOption { - type = types.str; - default = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; - defaultText = "\${config.system.build.initialRamdisk}/\${config.system.boot.loader.initrdFile}"; - description = '' - In direct boot situations, you may want to influence the initrd to load - to use your own customized payload. - - This is useful if you want to test the netboot image without - testing the firmware or the loading part. - ''; - }; - }; - - virtualisation.useBootLoader = - mkOption { + enable = mkOption { type = types.bool; - default = false; + default = !cfg.useBootLoader; + defaultText = "!cfg.useBootLoader"; description = '' - Use a boot loader to boot the system. - This allows, among other things, testing the boot loader. + If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. + Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html) - If disabled, the kernel and initrd are directly booted, - forgoing any bootloader. + This is enabled by default. + If you want to test netboot, consider disabling this option. + Enable a bootloader with {option}`virtualisation.useBootLoader` if you need. - Check the documentation on {option}`virtualisation.directBoot.enable` for details. - ''; - }; + Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU. + Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`. + They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details + For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`. - virtualisation.installBootLoader = - mkOption { - type = types.bool; - default = cfg.useBootLoader && cfg.useDefaultFilesystems; - defaultText = "cfg.useBootLoader && cfg.useDefaultFilesystems"; - description = '' - Install boot loader to target image. - - This is best-effort and may break with unconventional partition setups. - Use `virtualisation.useDefaultFilesystems` for a known-working configuration. + This will not (re-)boot correctly into a system that has switched to a different configuration on disk. ''; }; - - virtualisation.useEFIBoot = - mkOption { - type = types.bool; - default = false; + initrd = mkOption { + type = types.str; + default = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + defaultText = "\${config.system.build.initialRamdisk}/\${config.system.boot.loader.initrdFile}"; description = '' - If enabled, the virtual machine will provide a EFI boot - manager. - useEFIBoot is ignored if useBootLoader == false. - ''; - }; + In direct boot situations, you may want to influence the initrd to load + to use your own customized payload. + + This is useful if you want to test the netboot image without + testing the firmware or the loading part. + ''; + }; + }; + + virtualisation.useBootLoader = mkOption { + type = types.bool; + default = false; + description = '' + Use a boot loader to boot the system. + This allows, among other things, testing the boot loader. + + If disabled, the kernel and initrd are directly booted, + forgoing any bootloader. + + Check the documentation on {option}`virtualisation.directBoot.enable` for details. + ''; + }; + + virtualisation.installBootLoader = mkOption { + type = types.bool; + default = cfg.useBootLoader && cfg.useDefaultFilesystems; + defaultText = "cfg.useBootLoader && cfg.useDefaultFilesystems"; + description = '' + Install boot loader to target image. + + This is best-effort and may break with unconventional partition setups. + Use `virtualisation.useDefaultFilesystems` for a known-working configuration. + ''; + }; + + virtualisation.useEFIBoot = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, the virtual machine will provide a EFI boot + manager. + useEFIBoot is ignored if useBootLoader == false. + ''; + }; virtualisation.efi = { OVMF = mkOption { type = types.package; - default = (pkgs.OVMF.override { - secureBoot = cfg.useSecureBoot; - }).fd; - defaultText = ''(pkgs.OVMF.override { - secureBoot = cfg.useSecureBoot; - }).fd''; + default = + (pkgs.OVMF.override { + secureBoot = cfg.useSecureBoot; + }).fd; + defaultText = '' + (pkgs.OVMF.override { + secureBoot = cfg.useSecureBoot; + }).fd''; description = "OVMF firmware package, defaults to OVMF configured with secure boot if needed."; }; @@ -874,8 +939,8 @@ in default = cfg.efi.OVMF.firmware; defaultText = literalExpression "cfg.efi.OVMF.firmware"; description = '' - Firmware binary for EFI implementation, defaults to OVMF. - ''; + Firmware binary for EFI implementation, defaults to OVMF. + ''; }; variables = mkOption { @@ -883,9 +948,9 @@ in default = cfg.efi.OVMF.variables; defaultText = literalExpression "cfg.efi.OVMF.variables"; description = '' - Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware. - Defaults to OVMF. - ''; + Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware. + Defaults to OVMF. + ''; }; keepVariables = mkOption { @@ -903,13 +968,16 @@ in deviceModel = mkOption { type = types.str; - default = ({ - "i686-linux" = "tpm-tis"; - "x86_64-linux" = "tpm-tis"; - "ppc64-linux" = "tpm-spapr"; - "armv7-linux" = "tpm-tis-device"; - "aarch64-linux" = "tpm-tis-device"; - }.${pkgs.stdenv.hostPlatform.system} or (throw "Unsupported system for TPM2 emulation in QEMU")); + default = ( + { + "i686-linux" = "tpm-tis"; + "x86_64-linux" = "tpm-tis"; + "ppc64-linux" = "tpm-spapr"; + "armv7-linux" = "tpm-tis-device"; + "aarch64-linux" = "tpm-tis-device"; + } + .${pkgs.stdenv.hostPlatform.system} or (throw "Unsupported system for TPM2 emulation in QEMU") + ); defaultText = '' Based on the guest platform Linux system: @@ -922,104 +990,104 @@ in }; }; - virtualisation.useDefaultFilesystems = - mkOption { - type = types.bool; - default = true; - description = '' - If enabled, the boot disk of the virtual machine will be - formatted and mounted with the default filesystems for - testing. Swap devices and LUKS will be disabled. + virtualisation.useDefaultFilesystems = mkOption { + type = types.bool; + default = true; + description = '' + If enabled, the boot disk of the virtual machine will be + formatted and mounted with the default filesystems for + testing. Swap devices and LUKS will be disabled. - If disabled, a root filesystem has to be specified and - formatted (for example in the initial ramdisk). - ''; - }; + If disabled, a root filesystem has to be specified and + formatted (for example in the initial ramdisk). + ''; + }; - virtualisation.useSecureBoot = - mkOption { - type = types.bool; - default = false; - description = '' - Enable Secure Boot support in the EFI firmware. - ''; - }; + virtualisation.useSecureBoot = mkOption { + type = types.bool; + default = false; + description = '' + Enable Secure Boot support in the EFI firmware. + ''; + }; - virtualisation.bios = - mkOption { - type = types.nullOr types.package; - default = null; - description = '' - An alternate BIOS (such as `qboot`) with which to start the VM. - Should contain a file named `bios.bin`. - If `null`, QEMU's builtin SeaBIOS will be used. - ''; - }; + virtualisation.bios = mkOption { + type = types.nullOr types.package; + default = null; + description = '' + An alternate BIOS (such as `qboot`) with which to start the VM. + Should contain a file named `bios.bin`. + If `null`, QEMU's builtin SeaBIOS will be used. + ''; + }; - virtualisation.useHostCerts = - mkOption { - type = types.bool; - default = false; - description = '' - If enabled, when `NIX_SSL_CERT_FILE` is set on the host, - pass the CA certificates from the host to the VM. - ''; - }; + virtualisation.useHostCerts = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, when `NIX_SSL_CERT_FILE` is set on the host, + pass the CA certificates from the host to the VM. + ''; + }; }; config = { assertions = - lib.concatLists (lib.flip lib.imap cfg.forwardPorts (i: rule: - [ - { assertion = rule.from == "guest" -> rule.proto == "tcp"; - message = - '' + lib.concatLists ( + lib.flip lib.imap cfg.forwardPorts ( + i: rule: [ + { + assertion = rule.from == "guest" -> rule.proto == "tcp"; + message = '' Invalid virtualisation.forwardPorts..proto: Guest forwarding supports only TCP connections. ''; - } - { assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address; - message = - '' + } + { + assertion = rule.from == "guest" -> lib.hasPrefix "10.0.2." rule.guest.address; + message = '' Invalid virtualisation.forwardPorts..guest.address: The address must be in the default VLAN (10.0.2.0/24). ''; - } - ])) ++ [ - { assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047; - message = '' - virtualisation.memorySize is above 2047, but qemu is only able to allocate 2047MB RAM on 32bit max. - ''; - } - { assertion = cfg.directBoot.enable || cfg.directBoot.initrd == options.virtualisation.directBoot.initrd.default; - message = - '' - You changed the default of `virtualisation.directBoot.initrd` but you are not - using QEMU direct boot. This initrd will not be used in your current - boot configuration. + } + ] + ) + ) + ++ [ + { + assertion = pkgs.stdenv.hostPlatform.is32bit -> cfg.memorySize < 2047; + message = '' + virtualisation.memorySize is above 2047, but qemu is only able to allocate 2047MB RAM on 32bit max. + ''; + } + { + assertion = + cfg.directBoot.enable || cfg.directBoot.initrd == options.virtualisation.directBoot.initrd.default; + message = '' + You changed the default of `virtualisation.directBoot.initrd` but you are not + using QEMU direct boot. This initrd will not be used in your current + boot configuration. - Either do not mutate `virtualisation.directBoot.initrd` or enable direct boot. + Either do not mutate `virtualisation.directBoot.initrd` or enable direct boot. - If you have a more advanced usecase, please open an issue or a pull request. - ''; - } - { - assertion = cfg.installBootLoader -> config.system.switch.enable; - message = '' - `system.switch.enable` must be enabled for `virtualisation.installBootLoader` to work. - Please enable it in your configuration. - ''; - } - ]; + If you have a more advanced usecase, please open an issue or a pull request. + ''; + } + { + assertion = cfg.installBootLoader -> config.system.switch.enable; + message = '' + `system.switch.enable` must be enabled for `virtualisation.installBootLoader` to work. + Please enable it in your configuration. + ''; + } + ]; - warnings = - optional (cfg.directBoot.enable && cfg.useBootLoader) - '' - You enabled direct boot and a bootloader, QEMU will not boot your bootloader, rendering - `useBootLoader` useless. You might want to disable one of those options. - ''; + warnings = optional (cfg.directBoot.enable && cfg.useBootLoader) '' + You enabled direct boot and a bootloader, QEMU will not boot your bootloader, rendering + `useBootLoader` useless. You might want to disable one of those options. + ''; # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs. @@ -1037,12 +1105,11 @@ in # allow `system.build.toplevel' to be included. (If we had a direct # reference to ${regInfo} here, then we would get a cyclic # dependency.) - boot.postBootCommands = lib.mkIf config.nix.enable - '' - if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then - ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} - fi - ''; + boot.postBootCommands = lib.mkIf config.nix.enable '' + if [[ "$(cat /proc/cmdline)" =~ regInfo=([^ ]*) ]]; then + ${config.nix.package.out}/bin/nix-store --load-db < ''${BASH_REMATCH[1]} + fi + ''; boot.initrd.availableKernelModules = optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx" @@ -1079,14 +1146,20 @@ in virtualisation.qemu.networkingOptions = let - forwardingOptions = flip concatMapStrings cfg.forwardPorts - ({ proto, from, host, guest }: - if from == "host" - then "hostfwd=${proto}:${host.address}:${toString host.port}-" + - "${guest.address}:${toString guest.port}," - else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" + - "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}'," - ); + forwardingOptions = flip concatMapStrings cfg.forwardPorts ( + { + proto, + from, + host, + guest, + }: + if from == "host" then + "hostfwd=${proto}:${host.address}:${toString host.port}-" + + "${guest.address}:${toString guest.port}," + else + "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" + + "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}'," + ); restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,"; in [ @@ -1099,20 +1172,29 @@ in "-device virtio-keyboard" ]) (mkIf pkgs.stdenv.hostPlatform.isx86 [ - "-usb" "-device usb-tablet,bus=usb-bus.0" + "-usb" + "-device usb-tablet,bus=usb-bus.0" ]) (mkIf pkgs.stdenv.hostPlatform.isAarch [ - "-device virtio-gpu-pci" "-device usb-ehci,id=usb0" "-device usb-kbd" "-device usb-tablet" - ]) - (let - alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9)); - # Replace all non-alphanumeric characters with underscores - sanitizeShellIdent = s: concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s); - in mkIf cfg.directBoot.enable [ - "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}" - "-initrd ${cfg.directBoot.initrd}" - ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' + "-device virtio-gpu-pci" + "-device usb-ehci,id=usb0" + "-device usb-kbd" + "-device usb-tablet" ]) + ( + let + alphaNumericChars = lowerChars ++ upperChars ++ (map toString (range 0 9)); + # Replace all non-alphanumeric characters with underscores + sanitizeShellIdent = + s: + concatMapStrings (c: if builtins.elem c alphaNumericChars then c else "_") (stringToCharacters s); + in + mkIf cfg.directBoot.enable [ + "-kernel \${NIXPKGS_QEMU_KERNEL_${sanitizeShellIdent config.system.name}:-${config.system.build.toplevel}/kernel}" + "-initrd ${cfg.directBoot.initrd}" + ''-append "$(cat ${config.system.build.toplevel}/kernel-params) init=${config.system.build.toplevel}/init regInfo=${regInfo}/registration ${consoles} $QEMU_KERNEL_PARAMS"'' + ] + ) (mkIf cfg.useEFIBoot [ "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}" "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS" @@ -1129,26 +1211,32 @@ in "-device ${cfg.tpm.deviceModel},tpmdev=tpm_dev_0" ]) (mkIf (pkgs.stdenv.hostPlatform.isx86 && cfg.efi.OVMF.systemManagementModeRequired) [ - "-machine" "q35,smm=on" - "-global" "driver=cfi.pflash01,property=secure,value=on" + "-machine" + "q35,smm=on" + "-global" + "driver=cfi.pflash01,property=secure,value=on" ]) ]; virtualisation.qemu.drives = mkMerge [ - (mkIf (cfg.diskImage != null) [{ - name = "root"; - file = ''"$NIX_DISK_IMAGE"''; - driveExtraOpts.cache = "writeback"; - driveExtraOpts.werror = "report"; - deviceExtraOpts.bootindex = "1"; - deviceExtraOpts.serial = rootDriveSerialAttr; - }]) - (mkIf cfg.useNixStoreImage [{ - name = "nix-store"; - file = ''"$TMPDIR"/store.img''; - deviceExtraOpts.bootindex = "2"; - driveExtraOpts.format = "raw"; - }]) + (mkIf (cfg.diskImage != null) [ + { + name = "root"; + file = ''"$NIX_DISK_IMAGE"''; + driveExtraOpts.cache = "writeback"; + driveExtraOpts.werror = "report"; + deviceExtraOpts.bootindex = "1"; + deviceExtraOpts.serial = rootDriveSerialAttr; + } + ]) + (mkIf cfg.useNixStoreImage [ + { + name = "nix-store"; + file = ''"$TMPDIR"/store.img''; + deviceExtraOpts.bootindex = "2"; + driveExtraOpts.format = "raw"; + } + ]) (imap0 (idx: _: { file = "$(pwd)/empty${toString idx}.qcow2"; driveExtraOpts.werror = "report"; @@ -1162,91 +1250,114 @@ in # override by setting `virtualisation.fileSystems = lib.mkForce { };`. fileSystems = lib.mkIf (cfg.fileSystems != { }) (mkVMOverride cfg.fileSystems); - virtualisation.fileSystems = let - mkSharedDir = tag: share: - { + virtualisation.fileSystems = + let + mkSharedDir = tag: share: { name = share.target; value.device = tag; value.fsType = "9p"; value.neededForBoot = true; - value.options = - [ "trans=virtio" "version=9p2000.L" "msize=${toString cfg.msize}" "x-systemd.requires=modprobe@9pnet_virtio.service" ] - ++ lib.optional (tag == "nix-store") "cache=loose"; + value.options = [ + "trans=virtio" + "version=9p2000.L" + "msize=${toString cfg.msize}" + "x-systemd.requires=modprobe@9pnet_virtio.service" + ] ++ lib.optional (tag == "nix-store") "cache=loose"; }; - in lib.mkMerge [ - (lib.mapAttrs' mkSharedDir cfg.sharedDirectories) - { - "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then { - device = "tmpfs"; - fsType = "tmpfs"; - } else { - device = cfg.rootDevice; - fsType = "ext4"; - }); - "/tmp" = lib.mkIf config.boot.tmp.useTmpfs { - device = "tmpfs"; - fsType = "tmpfs"; - neededForBoot = true; - # Sync with systemd's tmp.mount; - options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ]; - }; - "/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) (if cfg.writableStore then { - overlay = { - lowerdir = [ "/nix/.ro-store" ]; - upperdir = "/nix/.rw-store/upper"; - workdir = "/nix/.rw-store/work"; + in + lib.mkMerge [ + (lib.mapAttrs' mkSharedDir cfg.sharedDirectories) + { + "/" = lib.mkIf cfg.useDefaultFilesystems ( + if cfg.diskImage == null then + { + device = "tmpfs"; + fsType = "tmpfs"; + } + else + { + device = cfg.rootDevice; + fsType = "ext4"; + } + ); + "/tmp" = lib.mkIf config.boot.tmp.useTmpfs { + device = "tmpfs"; + fsType = "tmpfs"; + neededForBoot = true; + # Sync with systemd's tmp.mount; + options = [ + "mode=1777" + "strictatime" + "nosuid" + "nodev" + "size=${toString config.boot.tmp.tmpfsSize}" + ]; }; - } else { - device = "/nix/.ro-store"; - options = [ "bind" ]; - }); - "/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage { - device = "/dev/disk/by-label/${nixStoreFilesystemLabel}"; - fsType = "erofs"; - neededForBoot = true; - options = [ "ro" ]; - }; - "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) { - fsType = "tmpfs"; - options = [ "mode=0755" ]; - neededForBoot = true; - }; - "${config.boot.loader.efi.efiSysMountPoint}" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) { - device = cfg.bootPartition; - fsType = "vfat"; - }; - } - ]; + "/nix/store" = lib.mkIf (cfg.useNixStoreImage || cfg.mountHostNixStore) ( + if cfg.writableStore then + { + overlay = { + lowerdir = [ "/nix/.ro-store" ]; + upperdir = "/nix/.rw-store/upper"; + workdir = "/nix/.rw-store/work"; + }; + } + else + { + device = "/nix/.ro-store"; + options = [ "bind" ]; + } + ); + "/nix/.ro-store" = lib.mkIf cfg.useNixStoreImage { + device = "/dev/disk/by-label/${nixStoreFilesystemLabel}"; + fsType = "erofs"; + neededForBoot = true; + options = [ "ro" ]; + }; + "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) { + fsType = "tmpfs"; + options = [ "mode=0755" ]; + neededForBoot = true; + }; + "${config.boot.loader.efi.efiSysMountPoint}" = + lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) + { + device = cfg.bootPartition; + fsType = "vfat"; + }; + } + ]; swapDevices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) [ ]; - boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) {}; + boot.initrd.luks.devices = (if cfg.useDefaultFilesystems then mkVMOverride else mkDefault) { }; # Don't run ntpd in the guest. It should get the correct time from KVM. services.timesyncd.enable = false; services.qemuGuest.enable = cfg.qemu.guestAgent.enable; - system.build.vm = hostPkgs.runCommand "nixos-vm" { - preferLocalBuild = true; - meta.mainProgram = "run-${config.system.name}-vm"; - } - '' - mkdir -p $out/bin - ln -s ${config.system.build.toplevel} $out/system - ln -s ${hostPkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm - ''; + system.build.vm = + hostPkgs.runCommand "nixos-vm" + { + preferLocalBuild = true; + meta.mainProgram = "run-${config.system.name}-vm"; + } + '' + mkdir -p $out/bin + ln -s ${config.system.build.toplevel} $out/system + ln -s ${hostPkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm + ''; # When building a regular system configuration, override whatever # video driver the host uses. services.xserver.videoDrivers = mkVMOverride [ "modesetting" ]; services.xserver.defaultDepth = mkVMOverride 0; services.xserver.resolutions = mkVMOverride [ cfg.resolution ]; - services.xserver.monitorSection = - '' - # Set a higher refresh rate so that resolutions > 800x600 work. - HorizSync 30-140 - VertRefresh 50-160 - ''; + services.xserver.monitorSection = '' + # Set a higher refresh rate so that resolutions > 800x600 work. + HorizSync 30-140 + VertRefresh 50-160 + ''; # Wireless won't work in the VM. networking.wireless.enable = mkVMOverride false; @@ -1257,8 +1368,10 @@ in networking.usePredictableInterfaceNames = false; - system.requiredKernelConfig = with config.lib.kernelConfig; - [ (isEnabled "VIRTIO_BLK") + system.requiredKernelConfig = + with config.lib.kernelConfig; + [ + (isEnabled "VIRTIO_BLK") (isEnabled "VIRTIO_PCI") (isEnabled "VIRTIO_NET") (isEnabled "EXT4_FS") @@ -1270,10 +1383,12 @@ in (isYes "NET_CORE") (isYes "INET") (isYes "NETWORK_FILESYSTEMS") - ] ++ optionals (!cfg.graphics) [ + ] + ++ optionals (!cfg.graphics) [ (isYes "SERIAL_8250_CONSOLE") (isYes "SERIAL_8250") - ] ++ optionals (cfg.writableStore) [ + ] + ++ optionals (cfg.writableStore) [ (isEnabled "OVERLAY_FS") ]; diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix index 4ab5d17ecd49..968f5b766e87 100644 --- a/nixos/modules/virtualisation/virtualbox-image.nix +++ b/nixos/modules/virtualisation/virtualbox-image.nix @@ -1,9 +1,15 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.virtualbox; -in { +in +{ options = { virtualbox = { @@ -51,7 +57,14 @@ in { ''; }; params = lib.mkOption { - type = with lib.types; attrsOf (oneOf [ str int bool (listOf str) ]); + type = + with lib.types; + attrsOf (oneOf [ + str + int + bool + (listOf str) + ]); example = { audio = "alsa"; rtcuseutc = "on"; @@ -64,11 +77,21 @@ in { ''; }; exportParams = lib.mkOption { - type = with lib.types; listOf (oneOf [ str int bool (listOf str) ]); + type = + with lib.types; + listOf (oneOf [ + str + int + bool + (listOf str) + ]); example = [ - "--vsys" "0" "--vendor" "ACME Inc." + "--vsys" + "0" + "--vendor" + "ACME Inc." ]; - default = []; + default = [ ]; description = '' Parameters passed to the Virtualbox export command. @@ -86,23 +109,25 @@ in { mountPoint = "/home/demo/storage"; size = 100 * 1024; }; - type = lib.types.nullOr (lib.types.submodule { - options = { - size = lib.mkOption { - type = lib.types.int; - description = "Size in MiB"; + type = lib.types.nullOr ( + lib.types.submodule { + options = { + size = lib.mkOption { + type = lib.types.int; + description = "Size in MiB"; + }; + label = lib.mkOption { + type = lib.types.str; + default = "vm-extra-storage"; + description = "Label for the disk partition"; + }; + mountPoint = lib.mkOption { + type = lib.types.str; + description = "Path where to mount this disk."; + }; }; - label = lib.mkOption { - type = lib.types.str; - default = "vm-extra-storage"; - description = "Label for the disk partition"; - }; - mountPoint = lib.mkOption { - type = lib.types.str; - description = "Path where to mount this disk."; - }; - }; - }); + } + ); }; postExportCommands = lib.mkOption { type = lib.types.lines; @@ -122,7 +147,14 @@ in { ''; }; storageController = lib.mkOption { - type = with lib.types; attrsOf (oneOf [ str int bool (listOf str) ]); + type = + with lib.types; + attrsOf (oneOf [ + str + int + bool + (listOf str) + ]); example = { name = "SCSI"; add = "scsi"; @@ -175,77 +207,80 @@ in { diskSize = cfg.baseImageSize; additionalSpace = "${toString cfg.baseImageFreeSpace}M"; - postVM = - '' - export HOME=$PWD - export PATH=${pkgs.virtualbox}/bin:$PATH + postVM = '' + export HOME=$PWD + export PATH=${pkgs.virtualbox}/bin:$PATH - echo "converting image to VirtualBox format..." - VBoxManage convertfromraw $diskImage disk.vdi + echo "converting image to VirtualBox format..." + VBoxManage convertfromraw $diskImage disk.vdi - ${lib.optionalString (cfg.extraDisk != null) '' - echo "creating extra disk: data-disk.raw" - dataDiskImage=data-disk.raw - truncate -s ${toString cfg.extraDisk.size}M $dataDiskImage + ${lib.optionalString (cfg.extraDisk != null) '' + echo "creating extra disk: data-disk.raw" + dataDiskImage=data-disk.raw + truncate -s ${toString cfg.extraDisk.size}M $dataDiskImage - parted --script $dataDiskImage -- \ - mklabel msdos \ - mkpart primary ext4 1MiB -1 - eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs) - mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K - echo "creating extra disk: data-disk.vdi" - VBoxManage convertfromraw $dataDiskImage data-disk.vdi - ''} + parted --script $dataDiskImage -- \ + mklabel msdos \ + mkpart primary ext4 1MiB -1 + eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs) + mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K + echo "creating extra disk: data-disk.vdi" + VBoxManage convertfromraw $dataDiskImage data-disk.vdi + ''} - echo "creating VirtualBox VM..." - vmName="${cfg.vmName}"; - VBoxManage createvm --name "$vmName" --register \ - --ostype ${if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "Linux26_64" else "Linux26"} - VBoxManage modifyvm "$vmName" \ - --memory ${toString cfg.memorySize} \ - ${lib.cli.toGNUCommandLineShell { } cfg.params} - VBoxManage storagectl "$vmName" ${lib.cli.toGNUCommandLineShell { } cfg.storageController} - VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 0 --device 0 --type hdd \ - --medium disk.vdi - ${lib.optionalString (cfg.extraDisk != null) '' - VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 1 --device 0 --type hdd \ - --medium data-disk.vdi - ''} + echo "creating VirtualBox VM..." + vmName="${cfg.vmName}"; + VBoxManage createvm --name "$vmName" --register \ + --ostype ${if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "Linux26_64" else "Linux26"} + VBoxManage modifyvm "$vmName" \ + --memory ${toString cfg.memorySize} \ + ${lib.cli.toGNUCommandLineShell { } cfg.params} + VBoxManage storagectl "$vmName" ${lib.cli.toGNUCommandLineShell { } cfg.storageController} + VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 0 --device 0 --type hdd \ + --medium disk.vdi + ${lib.optionalString (cfg.extraDisk != null) '' + VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 1 --device 0 --type hdd \ + --medium data-disk.vdi + ''} - echo "exporting VirtualBox VM..." - mkdir -p $out - fn="$out/${cfg.vmFileName}" - VBoxManage export "$vmName" --output "$fn" --options manifest ${lib.escapeShellArgs cfg.exportParams} - ${cfg.postExportCommands} + echo "exporting VirtualBox VM..." + mkdir -p $out + fn="$out/${cfg.vmFileName}" + VBoxManage export "$vmName" --output "$fn" --options manifest ${lib.escapeShellArgs cfg.exportParams} + ${cfg.postExportCommands} - rm -v $diskImage + rm -v $diskImage - mkdir -p $out/nix-support - echo "file ova $fn" >> $out/nix-support/hydra-build-products - ''; + mkdir -p $out/nix-support + echo "file ova $fn" >> $out/nix-support/hydra-build-products + ''; }; - fileSystems = { - "/" = { - device = "/dev/disk/by-label/nixos"; - autoResize = true; - fsType = "ext4"; - }; - } // (lib.optionalAttrs (cfg.extraDisk != null) { - ${cfg.extraDisk.mountPoint} = { - device = "/dev/disk/by-label/" + cfg.extraDisk.label; - autoResize = true; - fsType = "ext4"; - }; - }); + fileSystems = + { + "/" = { + device = "/dev/disk/by-label/nixos"; + autoResize = true; + fsType = "ext4"; + }; + } + // (lib.optionalAttrs (cfg.extraDisk != null) { + ${cfg.extraDisk.mountPoint} = { + device = "/dev/disk/by-label/" + cfg.extraDisk.label; + autoResize = true; + fsType = "ext4"; + }; + }); boot.growPartition = true; boot.loader.grub.device = "/dev/sda"; - swapDevices = [{ - device = "/var/swap"; - size = 2048; - }]; + swapDevices = [ + { + device = "/var/swap"; + size = 2048; + } + ]; virtualisation.virtualbox.guest.enable = true;