nixos/preload: init
[NixPkgs.git] / nixos / lib / make-single-disk-zfs-image.nix
bloba3564f9a8b68e05ac038a8e9b75cb2b21a9865f6
1 # Note: This is a private API, internal to NixOS. Its interface is subject
2 # to change without notice.
4 # The result of this builder is a single disk image, partitioned like this:
6 #  * partition #1: a very small, 1MiB partition to leave room for Grub.
8 #  * partition #2: boot, a partition formatted with FAT to be used for /boot.
9 #      FAT is chosen to support EFI.
11 #  * partition #3: nixos, a partition dedicated to a zpool.
13 # This single-disk approach does not satisfy ZFS's requirements for autoexpand,
14 # however automation can expand it anyway. For example, with
15 # `services.zfs.expandOnBoot`.
16 { lib
17 , pkgs
18 , # The NixOS configuration to be installed onto the disk image.
19   config
21 , # size of the FAT partition, in megabytes.
22   bootSize ? 1024
24 , # The size of the root partition, in megabytes.
25   rootSize ? 2048
27 , # The name of the ZFS pool
28   rootPoolName ? "tank"
30 , # zpool properties
31   rootPoolProperties ? {
32     autoexpand = "on";
33   }
34 , # pool-wide filesystem properties
35   rootPoolFilesystemProperties ? {
36     acltype = "posixacl";
37     atime = "off";
38     compression = "on";
39     mountpoint = "legacy";
40     xattr = "sa";
41   }
43 , # datasets, with per-attribute options:
44   # mount: (optional) mount point in the VM
45   # properties: (optional) ZFS properties on the dataset, like filesystemProperties
46   # Notes:
47   # 1. datasets will be created from shorter to longer names as a simple topo-sort
48   # 2. you should define a root's dataset's mount for `/`
49   datasets ? { }
51 , # The files and directories to be placed in the target file system.
52   # This is a list of attribute sets {source, target} where `source'
53   # is the file system object (regular file or directory) to be
54   # grafted in the file system at path `target'.
55   contents ? [ ]
57 , # The initial NixOS configuration file to be copied to
58   # /etc/nixos/configuration.nix. This configuration will be embedded
59   # inside a configuration which includes the described ZFS fileSystems.
60   configFile ? null
62 , # Shell code executed after the VM has finished.
63   postVM ? ""
65 , name ? "nixos-disk-image"
67 , # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
68   format ? "raw"
70 , # Include a copy of Nixpkgs in the disk image
71   includeChannel ? true
73 let
74   formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
76   compress = lib.optionalString (format == "qcow2-compressed") "-c";
78   filenameSuffix = "." + {
79     qcow2 = "qcow2";
80     vdi = "vdi";
81     vpc = "vhd";
82     raw = "img";
83   }.${formatOpt} or formatOpt;
84   rootFilename = "nixos.root${filenameSuffix}";
86   # FIXME: merge with channel.nix / make-channel.nix.
87   channelSources =
88     let
89       nixpkgs = lib.cleanSource pkgs.path;
90     in
91     pkgs.runCommand "nixos-${config.system.nixos.version}" { } ''
92       mkdir -p $out
93       cp -prd ${nixpkgs.outPath} $out/nixos
94       chmod -R u+w $out/nixos
95       if [ ! -e $out/nixos/nixpkgs ]; then
96         ln -s . $out/nixos/nixpkgs
97       fi
98       rm -rf $out/nixos/.git
99       echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
100     '';
102   closureInfo = pkgs.closureInfo {
103     rootPaths = [ config.system.build.toplevel ]
104       ++ (lib.optional includeChannel channelSources);
105   };
107   modulesTree = pkgs.aggregateModules
108     (with config.boot.kernelPackages; [ kernel zfs ]);
110   tools = lib.makeBinPath (
111     with pkgs; [
112       config.system.build.nixos-enter
113       config.system.build.nixos-install
114       dosfstools
115       e2fsprogs
116       gptfdisk
117       nix
118       parted
119       util-linux
120       zfs
121     ]
122   );
124   hasDefinedMount = disk: ((disk.mount or null) != null);
126   stringifyProperties = prefix: properties: lib.concatStringsSep " \\\n" (
127     lib.mapAttrsToList
128       (
129         property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
130       )
131       properties
132   );
134   createDatasets =
135     let
136       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
137       sorted = lib.sort (left: right: (lib.stringLength left.name) < (lib.stringLength right.name)) datasetlist;
138       cmd = { name, value }:
139         let
140           properties = stringifyProperties "-o" (value.properties or { });
141         in
142         "zfs create -p ${properties} ${name}";
143     in
144     lib.concatMapStringsSep "\n" cmd sorted;
146   mountDatasets =
147     let
148       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
149       mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
150       sorted = lib.sort (left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)) mounts;
151       cmd = { name, value }:
152         ''
153           mkdir -p /mnt${lib.escapeShellArg value.mount}
154           mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
155         '';
156     in
157     lib.concatMapStringsSep "\n" cmd sorted;
159   unmountDatasets =
160     let
161       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
162       mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
163       sorted = lib.sort (left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)) mounts;
164       cmd = { name, value }:
165         ''
166           umount /mnt${lib.escapeShellArg value.mount}
167         '';
168     in
169     lib.concatMapStringsSep "\n" cmd sorted;
172   fileSystemsCfgFile =
173     let
174       mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
175     in
176     pkgs.runCommand "filesystem-config.nix"
177       {
178         buildInputs = with pkgs; [ jq nixpkgs-fmt ];
179         filesystems = builtins.toJSON {
180           fileSystems = lib.mapAttrs'
181             (
182               dataset: attrs:
183                 {
184                   name = attrs.mount;
185                   value = {
186                     fsType = "zfs";
187                     device = "${dataset}";
188                   };
189                 }
190             )
191             mountable;
192         };
193         passAsFile = [ "filesystems" ];
194       } ''
195       (
196         echo "builtins.fromJSON '''"
197         jq . < "$filesystemsPath"
198         echo "'''"
199       ) > $out
201       nixpkgs-fmt $out
202     '';
204   mergedConfig =
205     if configFile == null
206     then fileSystemsCfgFile
207     else
208       pkgs.runCommand "configuration.nix"
209         {
210           buildInputs = with pkgs; [ nixpkgs-fmt ];
211         }
212         ''
213           (
214             echo '{ imports = ['
215             printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
216             printf "(%s)\n" "$(cat ${configFile})";
217             echo ']; }'
218           ) > $out
220           nixpkgs-fmt $out
221         '';
223   image = (
224     pkgs.vmTools.override {
225       rootModules =
226         [ "zfs" "9p" "9pnet_virtio" "virtio_pci" "virtio_blk" ] ++
227         (pkgs.lib.optional pkgs.stdenv.hostPlatform.isx86 "rtc_cmos");
228       kernel = modulesTree;
229     }
230   ).runInLinuxVM (
231     pkgs.runCommand name
232       {
233         memSize = 1024;
234         QEMU_OPTS = "-drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
235         preVM = ''
236           PATH=$PATH:${pkgs.qemu_kvm}/bin
237           mkdir $out
239           rootDiskImage=root.raw
240           qemu-img create -f raw $rootDiskImage ${toString (bootSize + rootSize)}M
241         '';
243         postVM = ''
244             ${if formatOpt == "raw" then ''
245             mv $rootDiskImage $out/${rootFilename}
246           '' else ''
247             ${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
248           ''}
249             rootDiskImage=$out/${rootFilename}
250             set -x
251             ${postVM}
252         '';
253       } ''
254       export PATH=${tools}:$PATH
255       set -x
257       cp -sv /dev/vda /dev/sda
258       cp -sv /dev/vda /dev/xvda
260       parted --script /dev/vda -- \
261         mklabel gpt \
262         mkpart no-fs 1MiB 2MiB \
263         set 1 bios_grub on \
264         align-check optimal 1 \
265         mkpart primary fat32 2MiB ${toString bootSize}MiB \
266         align-check optimal 2 \
267         mkpart primary fat32 ${toString bootSize}MiB -1MiB \
268         align-check optimal 3 \
269         print
271       sfdisk --dump /dev/vda
274       zpool create \
275         ${stringifyProperties "  -o" rootPoolProperties} \
276         ${stringifyProperties "  -O" rootPoolFilesystemProperties} \
277         ${rootPoolName} /dev/vda3
278       parted --script /dev/vda -- print
280       ${createDatasets}
281       ${mountDatasets}
283       mkdir -p /mnt/boot
284       mkfs.vfat -n ESP /dev/vda2
285       mount /dev/vda2 /mnt/boot
287       mount
289       # Install a configuration.nix
290       mkdir -p /mnt/etc/nixos
291       # `cat` so it is mutable on the fs
292       cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
294       export NIX_STATE_DIR=$TMPDIR/state
295       nix-store --load-db < ${closureInfo}/registration
297       nixos-install \
298         --root /mnt \
299         --no-root-passwd \
300         --system ${config.system.build.toplevel} \
301         --substituters "" \
302         ${lib.optionalString includeChannel ''--channel ${channelSources}''}
304       df -h
306       umount /mnt/boot
307       ${unmountDatasets}
309       zpool export ${rootPoolName}
310     ''
311   );
313 image