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 &lt;pool-name&gt;", 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}