1{ config, lib, pkgs, utils, ... }:
2#
3# todo:
4# - crontab for scrubs, etc
5# - zfs tunables
6
7with utils;
8with lib;
9
10let
11
12 cfgSpl = config.boot.spl;
13 cfgZfs = config.boot.zfs;
14 cfgSnapshots = config.services.zfs.autoSnapshot;
15
16 inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems;
17 inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems;
18
19 enableAutoSnapshots = cfgSnapshots.enable;
20 enableZfs = inInitrd || inSystem || enableAutoSnapshots;
21
22 kernel = config.boot.kernelPackages;
23
24 splKernelPkg = kernel.spl;
25 zfsKernelPkg = kernel.zfs;
26 zfsUserPkg = pkgs.zfs;
27
28 autosnapPkg = pkgs.zfstools.override {
29 zfs = zfsUserPkg;
30 };
31
32 zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot";
33
34 datasetToPool = x: elemAt (splitString "/" x) 0;
35
36 fsToPool = fs: datasetToPool fs.device;
37
38 zfsFilesystems = filter (x: x.fsType == "zfs") (attrValues config.fileSystems);
39
40 isRoot = fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
41
42 allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools);
43
44 rootPools = unique (map fsToPool (filter isRoot zfsFilesystems));
45
46 dataPools = unique (filter (pool: !(elem pool rootPools)) allPools);
47
48in
49
50{
51
52 ###### interface
53
54 options = {
55 boot.zfs = {
56
57 extraPools = mkOption {
58 type = types.listOf types.str;
59 default = [];
60 example = [ "tank" "data" ];
61 description = ''
62 Name or GUID of extra ZFS pools that you wish to import during boot.
63
64 Usually this is not necessary. Instead, you should set the mountpoint property
65 of ZFS filesystems to <literal>legacy</literal> and add the ZFS filesystems to
66 NixOS's <option>fileSystems</option> option, which makes NixOS automatically
67 import the associated pool.
68
69 However, in some cases (e.g. if you have many filesystems) it may be preferable
70 to exclusively use ZFS commands to manage filesystems. If so, since NixOS/systemd
71 will not be managing those filesystems, you will need to specify the ZFS pool here
72 so that NixOS automatically imports it on every boot.
73 '';
74 };
75
76 forceImportRoot = mkOption {
77 type = types.bool;
78 default = true;
79 example = false;
80 description = ''
81 Forcibly import the ZFS root pool(s) during early boot.
82
83 This is enabled by default for backwards compatibility purposes, but it is highly
84 recommended to disable this option, as it bypasses some of the safeguards ZFS uses
85 to protect your ZFS pools.
86
87 If you set this option to <literal>false</literal> and NixOS subsequently fails to
88 boot because it cannot import the root pool, you should boot with the
89 <literal>zfs_force=1</literal> option as a kernel parameter (e.g. by manually
90 editing the kernel params in grub during boot). You should only need to do this
91 once.
92 '';
93 };
94
95 forceImportAll = mkOption {
96 type = types.bool;
97 default = true;
98 example = false;
99 description = ''
100 Forcibly import all ZFS pool(s).
101
102 This is enabled by default for backwards compatibility purposes, but it is highly
103 recommended to disable this option, as it bypasses some of the safeguards ZFS uses
104 to protect your ZFS pools.
105
106 If you set this option to <literal>false</literal> and NixOS subsequently fails to
107 import your non-root ZFS pool(s), you should manually import each pool with
108 "zpool import -f <pool-name>", and then reboot. You should only need to do
109 this once.
110 '';
111 };
112 };
113
114 services.zfs.autoSnapshot = {
115 enable = mkOption {
116 default = false;
117 type = types.bool;
118 description = ''
119 Enable the (OpenSolaris-compatible) ZFS auto-snapshotting service.
120 Note that you must set the <literal>com.sun:auto-snapshot</literal>
121 property to <literal>true</literal> on all datasets which you wish
122 to auto-snapshot.
123
124 You can override a child dataset to use, or not use auto-snapshotting
125 by setting its flag with the given interval:
126 <literal>zfs set com.sun:auto-snapshot:weekly=false DATASET</literal>
127 '';
128 };
129
130 frequent = mkOption {
131 default = 4;
132 type = types.int;
133 description = ''
134 Number of frequent (15-minute) auto-snapshots that you wish to keep.
135 '';
136 };
137
138 hourly = mkOption {
139 default = 24;
140 type = types.int;
141 description = ''
142 Number of hourly auto-snapshots that you wish to keep.
143 '';
144 };
145
146 daily = mkOption {
147 default = 7;
148 type = types.int;
149 description = ''
150 Number of daily auto-snapshots that you wish to keep.
151 '';
152 };
153
154 weekly = mkOption {
155 default = 4;
156 type = types.int;
157 description = ''
158 Number of weekly auto-snapshots that you wish to keep.
159 '';
160 };
161
162 monthly = mkOption {
163 default = 12;
164 type = types.int;
165 description = ''
166 Number of monthly auto-snapshots that you wish to keep.
167 '';
168 };
169 };
170 };
171
172 ###### implementation
173
174 config = mkMerge [
175 (mkIf enableZfs {
176 assertions = [
177 {
178 assertion = config.networking.hostId != null;
179 message = "ZFS requires config.networking.hostId to be set";
180 }
181 {
182 assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot;
183 message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot";
184 }
185 ];
186
187 boot = {
188 kernelModules = [ "spl" "zfs" ] ;
189 extraModulePackages = [ splKernelPkg zfsKernelPkg ];
190 };
191
192 boot.initrd = mkIf inInitrd {
193 kernelModules = [ "spl" "zfs" ];
194 extraUtilsCommands =
195 ''
196 copy_bin_and_libs ${zfsUserPkg}/sbin/zfs
197 copy_bin_and_libs ${zfsUserPkg}/sbin/zdb
198 copy_bin_and_libs ${zfsUserPkg}/sbin/zpool
199 '';
200 extraUtilsCommandsTest = mkIf inInitrd
201 ''
202 $out/bin/zfs --help >/dev/null 2>&1
203 $out/bin/zpool --help >/dev/null 2>&1
204 '';
205 postDeviceCommands = concatStringsSep "\n" ([''
206 ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}"
207
208 for o in $(cat /proc/cmdline); do
209 case $o in
210 zfs_force|zfs_force=1)
211 ZFS_FORCE="-f"
212 ;;
213 esac
214 done
215 ''] ++ (map (pool: ''
216 echo "importing root ZFS pool \"${pool}\"..."
217 zpool import -N $ZFS_FORCE "${pool}"
218 '') rootPools));
219 };
220
221 boot.loader.grub = mkIf inInitrd {
222 zfsSupport = true;
223 };
224
225 environment.etc."zfs/zed.d".source = "${zfsUserPkg}/etc/zfs/zed.d/*";
226
227 system.fsPackages = [ zfsUserPkg ]; # XXX: needed? zfs doesn't have (need) a fsck
228 environment.systemPackages = [ zfsUserPkg ];
229 services.udev.packages = [ zfsUserPkg ]; # to hook zvol naming, etc.
230 systemd.packages = [ zfsUserPkg ];
231
232 systemd.services = let
233 getPoolFilesystems = pool:
234 filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.fileSystems);
235
236 getPoolMounts = pool:
237 let
238 mountPoint = fs: escapeSystemdPath fs.mountPoint;
239 in
240 map (x: "${mountPoint x}.mount") (getPoolFilesystems pool);
241
242 createImportService = pool:
243 nameValuePair "zfs-import-${pool}" {
244 description = "Import ZFS pool \"${pool}\"";
245 requires = [ "systemd-udev-settle.service" ];
246 after = [ "systemd-udev-settle.service" "systemd-modules-load.service" ];
247 wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ];
248 before = (getPoolMounts pool) ++ [ "local-fs.target" ];
249 unitConfig = {
250 DefaultDependencies = "no";
251 };
252 serviceConfig = {
253 Type = "oneshot";
254 RemainAfterExit = true;
255 };
256 script = ''
257 zpool_cmd="${zfsUserPkg}/sbin/zpool"
258 ("$zpool_cmd" list "${pool}" >/dev/null) || "$zpool_cmd" import -N ${optionalString cfgZfs.forceImportAll "-f"} "${pool}"
259 '';
260 };
261 in listToAttrs (map createImportService dataPools) // {
262 "zfs-mount" = { after = [ "systemd-modules-load.service" ]; };
263 "zfs-share" = { after = [ "systemd-modules-load.service" ]; };
264 "zed" = { after = [ "systemd-modules-load.service" ]; };
265 };
266
267 systemd.targets."zfs-import" =
268 let
269 services = map (pool: "zfs-import-${pool}.service") dataPools;
270 in
271 {
272 requires = services;
273 after = services;
274 };
275
276 systemd.targets."zfs".wantedBy = [ "multi-user.target" ];
277 })
278
279 (mkIf enableAutoSnapshots {
280 systemd.services."zfs-snapshot-frequent" = {
281 description = "ZFS auto-snapshotting every 15 mins";
282 after = [ "zfs-import.target" ];
283 serviceConfig = {
284 Type = "oneshot";
285 ExecStart = "${zfsAutoSnap} frequent ${toString cfgSnapshots.frequent}";
286 };
287 restartIfChanged = false;
288 startAt = "*:15,30,45";
289 };
290
291 systemd.services."zfs-snapshot-hourly" = {
292 description = "ZFS auto-snapshotting every hour";
293 after = [ "zfs-import.target" ];
294 serviceConfig = {
295 Type = "oneshot";
296 ExecStart = "${zfsAutoSnap} hourly ${toString cfgSnapshots.hourly}";
297 };
298 restartIfChanged = false;
299 startAt = "hourly";
300 };
301
302 systemd.services."zfs-snapshot-daily" = {
303 description = "ZFS auto-snapshotting every day";
304 after = [ "zfs-import.target" ];
305 serviceConfig = {
306 Type = "oneshot";
307 ExecStart = "${zfsAutoSnap} daily ${toString cfgSnapshots.daily}";
308 };
309 restartIfChanged = false;
310 startAt = "daily";
311 };
312
313 systemd.services."zfs-snapshot-weekly" = {
314 description = "ZFS auto-snapshotting every week";
315 after = [ "zfs-import.target" ];
316 serviceConfig = {
317 Type = "oneshot";
318 ExecStart = "${zfsAutoSnap} weekly ${toString cfgSnapshots.weekly}";
319 };
320 restartIfChanged = false;
321 startAt = "weekly";
322 };
323
324 systemd.services."zfs-snapshot-monthly" = {
325 description = "ZFS auto-snapshotting every month";
326 after = [ "zfs-import.target" ];
327 serviceConfig = {
328 Type = "oneshot";
329 ExecStart = "${zfsAutoSnap} monthly ${toString cfgSnapshots.monthly}";
330 };
331 restartIfChanged = false;
332 startAt = "monthly";
333 };
334 })
335 ];
336}