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