Merge pull request #305886 from wegank/ocaml-jane-street-old-drop
[NixPkgs.git] / nixos / lib / make-single-disk-zfs-image.nix
blob585fa93b7fa0f79be1b55bebe83b8dfb81fb9576
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   , # memory allocated for virtualized build instance
25   memSize ? 1024
27 , # The size of the root partition, in megabytes.
28   rootSize ? 2048
30 , # The name of the ZFS pool
31   rootPoolName ? "tank"
33 , # zpool properties
34   rootPoolProperties ? {
35     autoexpand = "on";
36   }
37 , # pool-wide filesystem properties
38   rootPoolFilesystemProperties ? {
39     acltype = "posixacl";
40     atime = "off";
41     compression = "on";
42     mountpoint = "legacy";
43     xattr = "sa";
44   }
46 , # datasets, with per-attribute options:
47   # mount: (optional) mount point in the VM
48   # properties: (optional) ZFS properties on the dataset, like filesystemProperties
49   # Notes:
50   # 1. datasets will be created from shorter to longer names as a simple topo-sort
51   # 2. you should define a root's dataset's mount for `/`
52   datasets ? { }
54 , # The files and directories to be placed in the target file system.
55   # This is a list of attribute sets {source, target} where `source'
56   # is the file system object (regular file or directory) to be
57   # grafted in the file system at path `target'.
58   contents ? [ ]
60 , # The initial NixOS configuration file to be copied to
61   # /etc/nixos/configuration.nix. This configuration will be embedded
62   # inside a configuration which includes the described ZFS fileSystems.
63   configFile ? null
65 , # Shell code executed after the VM has finished.
66   postVM ? ""
68 , name ? "nixos-disk-image"
70 , # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
71   format ? "raw"
73 , # Include a copy of Nixpkgs in the disk image
74   includeChannel ? true
76 let
77   formatOpt = if format == "qcow2-compressed" then "qcow2" else format;
79   compress = lib.optionalString (format == "qcow2-compressed") "-c";
81   filenameSuffix = "." + {
82     qcow2 = "qcow2";
83     vdi = "vdi";
84     vpc = "vhd";
85     raw = "img";
86   }.${formatOpt} or formatOpt;
87   rootFilename = "nixos.root${filenameSuffix}";
89   # FIXME: merge with channel.nix / make-channel.nix.
90   channelSources =
91     let
92       nixpkgs = lib.cleanSource pkgs.path;
93     in
94     pkgs.runCommand "nixos-${config.system.nixos.version}" { } ''
95       mkdir -p $out
96       cp -prd ${nixpkgs.outPath} $out/nixos
97       chmod -R u+w $out/nixos
98       if [ ! -e $out/nixos/nixpkgs ]; then
99         ln -s . $out/nixos/nixpkgs
100       fi
101       rm -rf $out/nixos/.git
102       echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
103     '';
105   closureInfo = pkgs.closureInfo {
106     rootPaths = [ config.system.build.toplevel ]
107       ++ (lib.optional includeChannel channelSources);
108   };
110   modulesTree = pkgs.aggregateModules
111     (with config.boot.kernelPackages; [ kernel zfs ]);
113   tools = lib.makeBinPath (
114     with pkgs; [
115       config.system.build.nixos-enter
116       config.system.build.nixos-install
117       dosfstools
118       e2fsprogs
119       gptfdisk
120       nix
121       parted
122       util-linux
123       zfs
124     ]
125   );
127   hasDefinedMount = disk: ((disk.mount or null) != null);
129   stringifyProperties = prefix: properties: lib.concatStringsSep " \\\n" (
130     lib.mapAttrsToList
131       (
132         property: value: "${prefix} ${lib.escapeShellArg property}=${lib.escapeShellArg value}"
133       )
134       properties
135   );
137   createDatasets =
138     let
139       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
140       sorted = lib.sort (left: right: (lib.stringLength left.name) < (lib.stringLength right.name)) datasetlist;
141       cmd = { name, value }:
142         let
143           properties = stringifyProperties "-o" (value.properties or { });
144         in
145         "zfs create -p ${properties} ${name}";
146     in
147     lib.concatMapStringsSep "\n" cmd sorted;
149   mountDatasets =
150     let
151       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
152       mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
153       sorted = lib.sort (left: right: (lib.stringLength left.value.mount) < (lib.stringLength right.value.mount)) mounts;
154       cmd = { name, value }:
155         ''
156           mkdir -p /mnt${lib.escapeShellArg value.mount}
157           mount -t zfs ${name} /mnt${lib.escapeShellArg value.mount}
158         '';
159     in
160     lib.concatMapStringsSep "\n" cmd sorted;
162   unmountDatasets =
163     let
164       datasetlist = lib.mapAttrsToList lib.nameValuePair datasets;
165       mounts = lib.filter ({ value, ... }: hasDefinedMount value) datasetlist;
166       sorted = lib.sort (left: right: (lib.stringLength left.value.mount) > (lib.stringLength right.value.mount)) mounts;
167       cmd = { name, value }:
168         ''
169           umount /mnt${lib.escapeShellArg value.mount}
170         '';
171     in
172     lib.concatMapStringsSep "\n" cmd sorted;
175   fileSystemsCfgFile =
176     let
177       mountable = lib.filterAttrs (_: value: hasDefinedMount value) datasets;
178     in
179     pkgs.runCommand "filesystem-config.nix"
180       {
181         buildInputs = with pkgs; [ jq nixpkgs-fmt ];
182         filesystems = builtins.toJSON {
183           fileSystems = lib.mapAttrs'
184             (
185               dataset: attrs:
186                 {
187                   name = attrs.mount;
188                   value = {
189                     fsType = "zfs";
190                     device = "${dataset}";
191                   };
192                 }
193             )
194             mountable;
195         };
196         passAsFile = [ "filesystems" ];
197       } ''
198       (
199         echo "builtins.fromJSON '''"
200         jq . < "$filesystemsPath"
201         echo "'''"
202       ) > $out
204       nixpkgs-fmt $out
205     '';
207   mergedConfig =
208     if configFile == null
209     then fileSystemsCfgFile
210     else
211       pkgs.runCommand "configuration.nix"
212         {
213           buildInputs = with pkgs; [ nixpkgs-fmt ];
214         }
215         ''
216           (
217             echo '{ imports = ['
218             printf "(%s)\n" "$(cat ${fileSystemsCfgFile})";
219             printf "(%s)\n" "$(cat ${configFile})";
220             echo ']; }'
221           ) > $out
223           nixpkgs-fmt $out
224         '';
226   image = (
227     pkgs.vmTools.override {
228       rootModules =
229         [ "zfs" "9p" "9pnet_virtio" "virtio_pci" "virtio_blk" ] ++
230         (pkgs.lib.optional pkgs.stdenv.hostPlatform.isx86 "rtc_cmos");
231       kernel = modulesTree;
232     }
233   ).runInLinuxVM (
234     pkgs.runCommand name
235       {
236         inherit memSize;
237         QEMU_OPTS = "-drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
238         preVM = ''
239           PATH=$PATH:${pkgs.qemu_kvm}/bin
240           mkdir $out
242           rootDiskImage=root.raw
243           qemu-img create -f raw $rootDiskImage ${toString (bootSize + rootSize)}M
244         '';
246         postVM = ''
247             ${if formatOpt == "raw" then ''
248             mv $rootDiskImage $out/${rootFilename}
249           '' else ''
250             ${pkgs.qemu_kvm}/bin/qemu-img convert -f raw -O ${formatOpt} ${compress} $rootDiskImage $out/${rootFilename}
251           ''}
252             rootDiskImage=$out/${rootFilename}
253             set -x
254             ${postVM}
255         '';
256       } ''
257       export PATH=${tools}:$PATH
258       set -x
260       cp -sv /dev/vda /dev/sda
261       cp -sv /dev/vda /dev/xvda
263       parted --script /dev/vda -- \
264         mklabel gpt \
265         mkpart no-fs 1MiB 2MiB \
266         set 1 bios_grub on \
267         align-check optimal 1 \
268         mkpart primary fat32 2MiB ${toString bootSize}MiB \
269         align-check optimal 2 \
270         mkpart primary fat32 ${toString bootSize}MiB -1MiB \
271         align-check optimal 3 \
272         print
274       sfdisk --dump /dev/vda
277       zpool create \
278         ${stringifyProperties "  -o" rootPoolProperties} \
279         ${stringifyProperties "  -O" rootPoolFilesystemProperties} \
280         ${rootPoolName} /dev/vda3
281       parted --script /dev/vda -- print
283       ${createDatasets}
284       ${mountDatasets}
286       mkdir -p /mnt/boot
287       mkfs.vfat -n ESP /dev/vda2
288       mount /dev/vda2 /mnt/boot
290       mount
292       # Install a configuration.nix
293       mkdir -p /mnt/etc/nixos
294       # `cat` so it is mutable on the fs
295       cat ${mergedConfig} > /mnt/etc/nixos/configuration.nix
297       export NIX_STATE_DIR=$TMPDIR/state
298       nix-store --load-db < ${closureInfo}/registration
300       nixos-install \
301         --root /mnt \
302         --no-root-passwd \
303         --system ${config.system.build.toplevel} \
304         --substituters "" \
305         ${lib.optionalString includeChannel ''--channel ${channelSources}''}
307       df -h
309       umount /mnt/boot
310       ${unmountDatasets}
312       zpool export ${rootPoolName}
313     ''
314   );
316 image