at master 10 kB view raw
1# Note: This is a private API, internal to NixOS. Its interface is subject 2# to change without notice. 3# 4# The result of this builder is two disk images: 5# 6# * `boot` - a small disk formatted with FAT to be used for /boot. FAT is 7# chosen to support EFI. 8# * `root` - a larger disk with a zpool taking the entire disk. 9# 10# This two-disk approach is taken to satisfy ZFS's requirements for 11# autoexpand. 12# 13# # Why doesn't autoexpand work with ZFS in a partition? 14# 15# When ZFS owns the whole disk doesn’t really use a partition: it has 16# a marker partition at the start and a marker partition at the end of 17# the disk. 18# 19# If ZFS is constrained to a partition, ZFS leaves expanding the partition 20# up to the user. Obviously, the user may not choose to do so. 21# 22# Once the user expands the partition, calling zpool online -e expands the 23# vdev to use the whole partition. It doesn’t happen automatically 24# presumably because zed doesn’t get an event saying it’s partition grew, 25# whereas it can and does get an event saying the whole disk it is on is 26# now larger. 27{ 28 lib, 29 pkgs, 30 # The NixOS configuration to be installed onto the disk image. 31 config, 32 33 # size of the FAT boot disk in MiB (1024*1024 bytes) 34 bootSize ? 1024, 35 36 # The size of the root disk in MiB (1024*1024 bytes) 37 rootSize ? 2048, 38 39 # The name of the ZFS pool 40 rootPoolName ? "tank", 41 42 # zpool properties 43 rootPoolProperties ? { 44 autoexpand = "on"; 45 }, 46 # pool-wide filesystem properties 47 rootPoolFilesystemProperties ? { 48 acltype = "posixacl"; 49 atime = "off"; 50 compression = "on"; 51 mountpoint = "legacy"; 52 xattr = "sa"; 53 }, 54 55 # datasets, with per-attribute options: 56 # mount: (optional) mount point in the VM 57 # properties: (optional) ZFS properties on the dataset, like filesystemProperties 58 # Notes: 59 # 1. datasets will be created from shorter to longer names as a simple topo-sort 60 # 2. you should define a root's dataset's mount for `/` 61 datasets ? { }, 62 63 # The files and directories to be placed in the target file system. 64 # This is a list of attribute sets {source, target} where `source' 65 # is the file system object (regular file or directory) to be 66 # grafted in the file system at path `target'. 67 contents ? [ ], 68 69 # The initial NixOS configuration file to be copied to 70 # /etc/nixos/configuration.nix. This configuration will be embedded 71 # inside a configuration which includes the described ZFS fileSystems. 72 configFile ? null, 73 74 # Shell code executed after the VM has finished. 75 postVM ? "", 76 77 # Guest memory size in MiB (1024*1024 bytes) 78 memSize ? 1024, 79 80 name ? "nixos-disk-image", 81 82 # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw. 83 format ? "raw", 84 85 # Include a copy of Nixpkgs in the disk image 86 includeChannel ? true, 87}: 88let 89 formatOpt = if format == "qcow2-compressed" then "qcow2" else format; 90 91 compress = lib.optionalString (format == "qcow2-compressed") "-c"; 92 93 filenameSuffix = 94 "." 95 + { 96 qcow2 = "qcow2"; 97 vdi = "vdi"; 98 vpc = "vhd"; 99 raw = "img"; 100 } 101 .${formatOpt} or formatOpt; 102 bootFilename = "nixos.boot${filenameSuffix}"; 103 rootFilename = "nixos.root${filenameSuffix}"; 104 105 # FIXME: merge with channel.nix / make-channel.nix. 106 channelSources = 107 let 108 nixpkgs = lib.cleanSource pkgs.path; 109 in 110 pkgs.runCommand "nixos-${config.system.nixos.version}" { } '' 111 mkdir -p $out 112 cp -prd ${nixpkgs.outPath} $out/nixos 113 chmod -R u+w $out/nixos 114 if [ ! -e $out/nixos/nixpkgs ]; then 115 ln -s . $out/nixos/nixpkgs 116 fi 117 rm -rf $out/nixos/.git 118 echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix 119 ''; 120 121 closureInfo = pkgs.closureInfo { 122 rootPaths = [ config.system.build.toplevel ] ++ (lib.optional includeChannel channelSources); 123 }; 124 125 modulesTree = pkgs.aggregateModules ( 126 with config.boot; 127 [ 128 kernelPackages.kernel 129 kernelPackages.${pkgs.zfs.kernelModuleAttribute} 130 ] 131 ); 132 133 tools = lib.makeBinPath ( 134 with pkgs; 135 [ 136 nixos-enter 137 config.system.build.nixos-install 138 dosfstools 139 e2fsprogs 140 gptfdisk 141 nix 142 parted 143 util-linux 144 zfs 145 ] 146 ); 147 148 hasDefinedMount = disk: ((disk.mount or null) != null); 149 150 stringifyProperties = 151 prefix: properties: 152 lib.concatStringsSep " \\\n" ( 153 lib.mapAttrsToList ( 154 property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}" 155 ) properties 156 ); 157 158 createDatasets = 159 let 160 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets; 161 sorted = lib.sort ( 162 left: right: (lib.stringLength left.name) < (lib.stringLength right.name) 163 ) datasetlist; 164 cmd = 165 { name, value }: 166 let 167 properties = stringifyProperties "-o" (value.properties or { }); 168 in 169 "zfs create -p ${properties} ${name}"; 170 in 171 lib.concatMapStringsSep "\n" cmd sorted; 172 173 mountDatasets = 174 let 175 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets; 176 mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist; 177 sorted = lib.sort ( 178 left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount) 179 ) mounts; 180 cmd = 181 { name, value }: 182 '' 183 mkdir -p /mnt${lib.escapeShellArg value.mount} 184 mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount} 185 ''; 186 in 187 lib.concatMapStringsSep "\n" cmd sorted; 188 189 unmountDatasets = 190 let 191 datasetlist = lib.mapAttrsToList lib.nameValuePair datasets; 192 mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist; 193 sorted = lib.sort ( 194 left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount) 195 ) mounts; 196 cmd = 197 { name, value }: 198 '' 199 umount /mnt${lib.escapeShellArg value.mount} 200 ''; 201 in 202 lib.concatMapStringsSep "\n" cmd sorted; 203 204 fileSystemsCfgFile = 205 let 206 mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets; 207 in 208 pkgs.runCommand "filesystem-config.nix" 209 { 210 buildInputs = with pkgs; [ 211 jq 212 nixpkgs-fmt 213 ]; 214 filesystems = builtins.toJSON { 215 fileSystems = lib.mapAttrs' (dataset: attrs: { 216 name = attrs.mount; 217 value = { 218 fsType = "zfs"; 219 device = "${dataset}"; 220 }; 221 }) mountable; 222 }; 223 passAsFile = [ "filesystems" ]; 224 } 225 '' 226 ( 227 echo "builtins.fromJSON '''" 228 jq . < "$filesystemsPath" 229 echo "'''" 230 ) > $out 231 232 nixpkgs-fmt $out 233 ''; 234 235 mergedConfig = 236 if configFile == null then 237 fileSystemsCfgFile 238 else 239 pkgs.runCommand "configuration.nix" 240 { 241 buildInputs = with pkgs; [ nixpkgs-fmt ]; 242 } 243 '' 244 ( 245 echo '{ imports = [' 246 printf "(%s)\n" "$(cat ${fileSystemsCfgFile})"; 247 printf "(%s)\n" "$(cat ${configFile})"; 248 echo ']; }' 249 ) > $out 250 251 nixpkgs-fmt $out 252 ''; 253 254 image = 255 (pkgs.vmTools.override { 256 rootModules = [ 257 "9p" 258 "9pnet_virtio" 259 "virtio_blk" 260 "virtio_pci" 261 "virtiofs" 262 "zfs" 263 ]; 264 kernel = modulesTree; 265 }).runInLinuxVM 266 ( 267 pkgs.runCommand name 268 { 269 QEMU_OPTS = 270 "-drive file=$bootDiskImage,if=virtio,cache=unsafe,werror=report" 271 + " -drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report"; 272 inherit memSize; 273 preVM = '' 274 PATH=$PATH:${pkgs.qemu_kvm}/bin 275 mkdir $out 276 bootDiskImage=boot.raw 277 qemu-img create -f raw $bootDiskImage ${toString bootSize}M 278 279 rootDiskImage=root.raw 280 qemu-img create -f raw $rootDiskImage ${toString rootSize}M 281 ''; 282 283 postVM = '' 284 ${ 285 if formatOpt == "raw" then 286 '' 287 mv $bootDiskImage $out/${bootFilename} 288 mv $rootDiskImage $out/${rootFilename} 289 '' 290 else 291 '' 292 ${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $bootDiskImage $out/${bootFilename} 293 ${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename} 294 '' 295 } 296 bootDiskImage=$out/${bootFilename} 297 rootDiskImage=$out/${rootFilename} 298 set -x 299 ${postVM} 300 ''; 301 } 302 '' 303 export PATH=${tools}:$PATH 304 set -x 305 306 cp -sv /dev/vda /dev/sda 307 cp -sv /dev/vda /dev/xvda 308 309 parted --script /dev/vda -- \ 310 mklabel gpt \ 311 mkpart no-fs 1MiB 2MiB \ 312 set 1 bios_grub on \ 313 align-check optimal 1 \ 314 mkpart ESP fat32 2MiB -1MiB \ 315 align-check optimal 2 \ 316 print 317 318 sfdisk --dump /dev/vda 319 320 321 zpool create \ 322 ${stringifyProperties " -o" rootPoolProperties} \ 323 ${stringifyProperties " -O" rootPoolFilesystemProperties} \ 324 ${rootPoolName} /dev/vdb 325 parted --script /dev/vdb -- print 326 327 ${createDatasets} 328 ${mountDatasets} 329 330 mkdir -p /mnt/boot 331 mkfs.vfat -n ESP /dev/vda2 332 mount /dev/vda2 /mnt/boot 333 334 mount 335 336 # Install a configuration.nix 337 mkdir -p /mnt/etc/nixos 338 # `cat` so it is mutable on the fs 339 cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix 340 341 export NIX_STATE_DIR=$TMPDIR/state 342 nix-store --load-db < ${closureInfo}/registration 343 344 nixos-install \ 345 --root /mnt \ 346 --no-root-passwd \ 347 --system ${config.system.build.toplevel} \ 348 --substituters "" \ 349 ${lib.optionalString includeChannel ''--channel ${channelSources}''} 350 351 df -h 352 353 umount /mnt/boot 354 ${unmountDatasets} 355 356 zpool export ${rootPoolName} 357 '' 358 ); 359in 360image