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