├── README.md ├── create-disko-images-in-vm.nix ├── disko-images.nix ├── flake.lock └── flake.nix /README.md: -------------------------------------------------------------------------------- 1 | # Disko Images 2 | 3 | Create disk image files from NixOS + [disko](https://github.com/nix-community/disko) configuration. 4 | 5 | This is done by running `disko-create`, `disko-mount`and then`nixos-install` in a VM, where 6 | each `config.disko.devices.disk` is mounted as a qcow2 image. 7 | 8 | It heavily relies on qcow2 as to not create too large image files. Compression is optional. 9 | 10 | ## Usage 11 | 12 | Add disko-images as a NixOS module (using flakes): 13 | 14 | ```nix 15 | # flake.nix 16 | { 17 | inputs = { 18 | nixpkgs.url = "nixpkgs/nixos-22.11"; 19 | disko = { 20 | url = "github:nix-community/disko"; 21 | inputs.nixpkgs.follows = "nixpkgs"; 22 | }; 23 | disko-images.url = "github:chrillefkr/disko-images"; 24 | }; 25 | outputs = { self, nixpkgs, disko, disko-images, ... } @inputs: 26 | let 27 | pkgs = import nixpkgs { 28 | system = "x86_64-linux"; 29 | config.allowUnfree = true; 30 | }; 31 | in 32 | { 33 | nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem { 34 | inherit pkgs; 35 | specialArgs.inputs = inputs; 36 | modules = [ 37 | ./configuration.nix 38 | ./disko.nix 39 | disko.nixosModules.disko 40 | disko-images.nixosModules.disko-images 41 | ]; 42 | }; 43 | }; 44 | } 45 | ``` 46 | 47 | Build disko images using `nix build '.#nixosConfigurations.my-machine.config.system.build.diskoImages'`. 48 | 49 | Your disk image files appear at `./results/*.qcow2`. 50 | 51 | ## About 52 | 53 | First of all, this is a very simple (but working) way of creating disk images from NixOS + disko configuration. 54 | I've used it mainly to create Raspberry Pi SD card images. 55 | 56 | Inspired by: 57 | 58 | * https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/sd-card/sd-image-aarch64.nix 59 | * https://nixos.wiki/wiki/NixOps/Virtualization 60 | 61 | ## TODO 62 | 63 | * [ ] Ensure support for 64 | * [X] zfs 65 | * [ ] btrfs 66 | * [ ] lvm 67 | * [ ] Individual disk size (`config.diskoImages.diskAllocSizes`) 68 | * [ ] Create tests 69 | * [ ] Create examples 70 | 71 | ## Known issues 72 | 73 | ### Raspberry Pi Linux kernel 74 | 75 | The Raspberry Pi 4 Linux kernel (`pkgs.linuxPackages_rpi4`) (and problably kernels for the older boards) doesn't seem to work, as 76 | the kernel seems to lack support for 9pnet_virtio. It gives me the error message `9pnet_virtio: no channels available for device ` 77 | when it attemts to mount the nix store. 78 | 79 | A fix is to use official Linux kernel, e.g.: 80 | `boot.kernelPackages = pkgs.linuxPackages;` 81 | 82 | ## Contribution 83 | 84 | Please help. I'm quite new to Nix and NixOS, so any PR or issue is appreciated. 85 | -------------------------------------------------------------------------------- /create-disko-images-in-vm.nix: -------------------------------------------------------------------------------- 1 | { lib, pkgs, config, postVM ? "", size ? "2048M", includeChannel ? false, postInstallScript ? "", compress ? false, emulateUEFI ? false, memory ? 1024, }: 2 | let 3 | compress_args = if compress then "-c" else ""; 4 | channelSources = 5 | let 6 | nixpkgs = lib.cleanSource pkgs.path; 7 | in 8 | pkgs.runCommand "nixos-${config.system.nixos.version}" { } '' 9 | mkdir -p $out 10 | cp -prd ${nixpkgs.outPath} $out/nixos 11 | chmod -R u+w $out/nixos 12 | if [ ! -e $out/nixos/nixpkgs ]; then 13 | ln -s . $out/nixos/nixpkgs 14 | fi 15 | rm -rf $out/nixos/.git 16 | echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix 17 | ''; 18 | 19 | closureInfo = pkgs.closureInfo { 20 | rootPaths = [ config.system.build.toplevel ] 21 | ++ (lib.optional includeChannel channelSources); 22 | }; 23 | 24 | modulesTree = pkgs.aggregateModules 25 | (with config.boot.kernelPackages; [ kernel zfs ]); 26 | 27 | tools = lib.makeBinPath ( 28 | with pkgs; [ 29 | config.system.build.nixos-enter 30 | config.system.build.nixos-install 31 | dosfstools 32 | e2fsprogs 33 | gptfdisk 34 | nix 35 | parted 36 | util-linux 37 | zfs 38 | ] 39 | ); 40 | 41 | disk_paths = with builtins; map (disk: disk.device) (attrValues config.disko.devices.disk); 42 | disk_names = with builtins; attrNames config.disko.devices.disk; 43 | 44 | OVMF_CODE = "${pkgs.OVMF.fd}/" + ( if pkgs.system == "x86_64-linux" then "FV/OVMF_CODE.fd" else "FV/AAVMF_CODE.fd" ); 45 | OVMF_VARS = "${pkgs.OVMF.fd}/" + ( if pkgs.system == "x86_64-linux" then "FV/OVMF_VARS.fd" else "FV/AAVMF_VARS.fd" ); 46 | 47 | images = ( 48 | pkgs.vmTools.override { 49 | rootModules = 50 | [ "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_balloon" "virtio_rng" "ext4" "unix" "9p" "9pnet_virtio" "crc32c_generic" ] ++ 51 | [ "zfs" ] ++ 52 | (pkgs.lib.optional pkgs.stdenv.hostPlatform.isx86 "rtc_cmos"); 53 | kernel = modulesTree; 54 | } 55 | ).runInLinuxVM ( 56 | pkgs.runCommand "${config.system.name}" 57 | { 58 | memSize = memory; 59 | QEMU_OPTS = lib.strings.escapeShellArgs (lib.lists.flatten ( 60 | (builtins.map (disk_name: ["-drive" "file=${builtins.baseNameOf disk_name}.qcow2,if=virtio,cache=unsafe,werror=report"]) disk_names) 61 | ++ 62 | ( 63 | lib.optionals emulateUEFI [ 64 | #"-pflash" "${OVMF_CODE}" 65 | #"-bios" "${OVMF_CODE}" 66 | "-smbios" "type=0,uefi=on" 67 | "-smbios" "type=1,uuid=43d206e8-14eb-4011-bbba-be831e68e032" 68 | "-drive" "if=pflash,unit=0,format=raw,readonly=on,file=${OVMF_CODE}" 69 | "-drive" "if=pflash,unit=1,format=qcow2,id=drive-efidisk0,file=efidisk.qcow2" 70 | ] 71 | ) 72 | )); 73 | preVM = '' 74 | set -x 75 | PATH=$PATH:${pkgs.qemu_kvm}/bin 76 | mkdir -p $out 77 | 78 | '' + (lib.strings.optionalString emulateUEFI '' 79 | echo "Creating efidisk.qcow2 from ${OVMF_VARS} with size 64M" 80 | ${pkgs.qemu}/bin/qemu-img convert -cp -f raw -O qcow2 ${OVMF_VARS} efidisk.qcow2 81 | ${pkgs.qemu}/bin/qemu-img resize efidisk.qcow2 64M 82 | '') + '' 83 | 84 | for disk_image in ${ toString (map baseNameOf disk_names) }; do 85 | echo "Creating ''${disk_image}.qcow2 with size ${toString size}" 86 | ${pkgs.qemu}/bin/qemu-img create -f qcow2 "''${disk_image}.qcow2" ${toString size} 87 | done 88 | ''; 89 | 90 | postVM = '' 91 | set -x 92 | 93 | '' + (lib.strings.optionalString emulateUEFI '' 94 | echo Compressing efidisk.qcow2 95 | ${pkgs.qemu}/bin/qemu-img convert -cp -f qcow2 -O qcow2 efidisk.qcow2 ''${out}/efidisk.qcow2 96 | '') + '' 97 | 98 | for disk_image in ${ toString (map baseNameOf disk_names) }; do 99 | echo Compressing "''${disk_image}.qcow2" 100 | ${pkgs.qemu}/bin/qemu-img convert -p -f qcow2 -O qcow2 ${compress_args} "''${disk_image}.qcow2" "''${out}/''${disk_image}.qcow2" 101 | done 102 | ${postVM} 103 | ''; 104 | } ( 105 | '' 106 | export PATH=${tools}:$PATH 107 | set -x 108 | 109 | '' + (lib.strings.optionalString emulateUEFI '' 110 | # Mount efivars 111 | mount -t efivarfs efivarfs /sys/firmware/efi/efivars 112 | '') + '' 113 | 114 | # Create symlinks with disko device paths pointing to /dev/vdX 115 | # It's stupid, but it works 116 | local_disks=( /dev/vd{a..z} ) 117 | index=0 118 | for to in ${ toString disk_paths }; do 119 | from="''${local_disks[$index]}" 120 | if [ "$from" == "$to" ]; then continue; fi 121 | mkdir -p $( dirname $( realpath -s "$to" ) ) 122 | ln -vs "''${from}" "''${to}" 123 | for i in $(seq 0 10); do 124 | ln -vs "''${from}''${i}" "''${to}''${i}" 125 | ln -vs "''${from}''${i}" "''${to}p''${i}" 126 | ln -vs "''${from}''${i}" "''${to}-part''${i}" 127 | done 128 | index=$(( $index + 1 )) 129 | done 130 | 131 | # Run disko-create 132 | ${config.system.build.formatScript} 133 | # Run disko-mount 134 | ${config.system.build.mountScript} 135 | 136 | # Install NixOS 137 | 138 | export NIX_STATE_DIR=$TMPDIR/state 139 | nix-store --load-db < ${closureInfo}/registration 140 | 141 | nixos-install \ 142 | -v \ 143 | --root /mnt \ 144 | --no-root-passwd \ 145 | --substituters "" \ 146 | --option binary-caches "" \ 147 | --keep-going \ 148 | --system ${config.system.build.toplevel} \ 149 | ${lib.optionalString includeChannel ''--channel ${channelSources}''} 150 | 151 | # Run postInstallScript 152 | ${postInstallScript} 153 | 154 | # Clean /tmp 155 | find /mnt/tmp -mindepth 1 -delete 156 | 157 | # Clean up disk from unused sectors 158 | fstrim -av 159 | 160 | # Unmount all filesystems 161 | umount -Rv /mnt || : 162 | 163 | # Trim and export all zfs zpools 164 | for zpool in ${toString ( builtins.attrNames config.disko.devices.zpool ) }; do 165 | echo Trimming zpool "$zpool" 166 | zpool trim -w "$zpool" 167 | echo Exporting zpool "$zpool" 168 | zpool export "$zpool" 169 | done 170 | 171 | # Disconnect all volume groups 172 | # for zpool in ''${toString ( builtins.attrNames (lib.attrValues config.disko.devices.zpool))}; do 173 | 174 | # done 175 | '') 176 | ); 177 | in 178 | { 179 | inherit images; 180 | } 181 | -------------------------------------------------------------------------------- /disko-images.nix: -------------------------------------------------------------------------------- 1 | { self, config, lib, pkgs, modulesPath, specialArgs, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | origConfig = config; 7 | diskoImages = import ./create-disko-images-in-vm.nix rec { 8 | inherit lib pkgs; 9 | config = origConfig; 10 | size = cfg.defaultDiskAllocSize; 11 | inherit (cfg) compress emulateUEFI includeChannel memory; 12 | }; 13 | cfg = config.diskoImages; 14 | in 15 | { 16 | options.diskoImages = { 17 | compress = mkOption { 18 | default = true; 19 | description = lib.mdDoc '' 20 | Compresses qcow2 image as final step when enabled 21 | ''; 22 | }; 23 | includeChannel = mkOption { 24 | default = true; 25 | description = lib.mdDoc '' 26 | Whether to install nixpkgs. 27 | ''; 28 | }; 29 | memory = mkOption { 30 | default = 1024; 31 | description = lib.mdDoc '' 32 | How much memory (RAM) in MiB to allocate for VM during build. 33 | Some builds require more memory. 34 | ''; 35 | }; 36 | defaultDiskAllocSize = mkOption { 37 | default = "2048M"; 38 | description = lib.mdDoc '' 39 | Default initial qcow2 image file size allocation (in MB) for each disk in disko.devices.disk. 40 | Use config.diskoImages.diskAllocSizes for specific allocation sizes per disk. 41 | ''; 42 | }; 43 | diskAllocSizes = mkOption { 44 | default = {}; 45 | description = lib.mdDoc '' 46 | Initial qcow2 image file size allocation (in MB) for each disko.devices.disk.xyz. Defaults to config.diskoImages.defaultDiskAllocSize 47 | ''; 48 | }; 49 | emulateUEFI = mkOption { 50 | default = config.boot.loader.efi.canTouchEfiVariables; 51 | type = types.bool; 52 | defaultText = literalExpression "config.boot.loader.efi.canTouchEfiVariables"; 53 | description = lib.mdDoc '' 54 | If true will emulate UEFI for storing EFI variables, e.g. boot entries. Variables will be stored as efidisk.qcow2 55 | ''; 56 | }; 57 | }; 58 | 59 | config = { 60 | system.build.diskoPartyImages = diskoImages.images; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "root": {} 4 | }, 5 | "root": "root", 6 | "version": 7 7 | } 8 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | #nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | #nixlib.url = "github:nix-community/nixpkgs.lib"; 5 | # disko = { 6 | # url = "github:nix-community/disko"; 7 | # inputs.nixpkgs.follows = "nixpkgs"; 8 | # }; 9 | }; 10 | outputs = { self }@inputs: 11 | { 12 | nixosModules = rec { 13 | disko-images = ./disko-images.nix; 14 | default = disko-images; 15 | }; 16 | }; 17 | } 18 | --------------------------------------------------------------------------------