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