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