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 cfgZfs = config.boot.zfs; 13 cfgSnapshots = config.services.zfs.autoSnapshot; 14 cfgSnapFlags = cfgSnapshots.flags; 15 cfgScrub = config.services.zfs.autoScrub; 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 enableAutoScrub = cfgScrub.enable; 22 enableZfs = inInitrd || inSystem || enableAutoSnapshots || enableAutoScrub; 23 24 kernel = config.boot.kernelPackages; 25 26 packages = if config.boot.zfs.enableUnstable then { 27 spl = null; 28 zfs = kernel.zfsUnstable; 29 zfsUser = pkgs.zfsUnstable; 30 } else { 31 spl = kernel.spl; 32 zfs = kernel.zfs; 33 zfsUser = pkgs.zfs; 34 }; 35 36 autosnapPkg = pkgs.zfstools.override { 37 zfs = packages.zfsUser; 38 }; 39 40 zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot"; 41 42 datasetToPool = x: elemAt (splitString "/" x) 0; 43 44 fsToPool = fs: datasetToPool fs.device; 45 46 zfsFilesystems = filter (x: x.fsType == "zfs") config.system.build.fileSystems; 47 48 allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools); 49 50 rootPools = unique (map fsToPool (filter fsNeededForBoot zfsFilesystems)); 51 52 dataPools = unique (filter (pool: !(elem pool rootPools)) allPools); 53 54 snapshotNames = [ "frequent" "hourly" "daily" "weekly" "monthly" ]; 55 56 # When importing ZFS pools, there's one difficulty: These scripts may run 57 # before the backing devices (physical HDDs, etc.) of the pool have been 58 # scanned and initialized. 59 # 60 # An attempted import with all devices missing will just fail, and can be 61 # retried, but an import where e.g. two out of three disks in a three-way 62 # mirror are missing, will succeed. This is a problem: When the missing disks 63 # are later discovered, they won't be automatically set online, rendering the 64 # pool redundancy-less (and far slower) until such time as the system reboots. 65 # 66 # The solution is the below. poolReady checks the status of an un-imported 67 # pool, to see if *every* device is available -- in which case the pool will be 68 # in state ONLINE, as opposed to DEGRADED, FAULTED or MISSING. 69 # 70 # The import scripts then loop over this, waiting until the pool is ready or a 71 # sufficient amount of time has passed that we can assume it won't be. In the 72 # latter case it makes one last attempt at importing, allowing the system to 73 # (eventually) boot even with a degraded pool. 74 importLib = {zpoolCmd, awkCmd, cfgZfs}: '' 75 poolReady() { 76 pool="$1" 77 state="$("${zpoolCmd}" import | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")" 78 if [[ "$state" = "ONLINE" ]]; then 79 return 0 80 else 81 echo "Pool $pool in state $state, waiting" 82 return 1 83 fi 84 } 85 poolImported() { 86 pool="$1" 87 "${zpoolCmd}" list "$pool" >/dev/null 2>/dev/null 88 } 89 poolImport() { 90 pool="$1" 91 "${zpoolCmd}" import -d "${cfgZfs.devNodes}" -N $ZFS_FORCE "$pool" 92 } 93 ''; 94 95in 96 97{ 98 99 ###### interface 100 101 options = { 102 boot.zfs = { 103 enableUnstable = mkOption { 104 type = types.bool; 105 default = false; 106 description = '' 107 Use the unstable zfs package. This might be an option, if the latest 108 kernel is not yet supported by a published release of ZFS. Enabling 109 this option will install a development version of ZFS on Linux. The 110 version will have already passed an extensive test suite, but it is 111 more likely to hit an undiscovered bug compared to running a released 112 version of ZFS on Linux. 113 ''; 114 }; 115 116 extraPools = mkOption { 117 type = types.listOf types.str; 118 default = []; 119 example = [ "tank" "data" ]; 120 description = '' 121 Name or GUID of extra ZFS pools that you wish to import during boot. 122 123 Usually this is not necessary. Instead, you should set the mountpoint property 124 of ZFS filesystems to <literal>legacy</literal> and add the ZFS filesystems to 125 NixOS's <option>fileSystems</option> option, which makes NixOS automatically 126 import the associated pool. 127 128 However, in some cases (e.g. if you have many filesystems) it may be preferable 129 to exclusively use ZFS commands to manage filesystems. If so, since NixOS/systemd 130 will not be managing those filesystems, you will need to specify the ZFS pool here 131 so that NixOS automatically imports it on every boot. 132 ''; 133 }; 134 135 devNodes = mkOption { 136 type = types.path; 137 default = "/dev/disk/by-id"; 138 example = "/dev/disk/by-id"; 139 description = '' 140 Name of directory from which to import ZFS devices. 141 142 This should be a path under /dev containing stable names for all devices needed, as 143 import may fail if device nodes are renamed concurrently with a device failing. 144 ''; 145 }; 146 147 forceImportRoot = mkOption { 148 type = types.bool; 149 default = true; 150 description = '' 151 Forcibly import the ZFS root pool(s) during early boot. 152 153 This is enabled by default for backwards compatibility purposes, but it is highly 154 recommended to disable this option, as it bypasses some of the safeguards ZFS uses 155 to protect your ZFS pools. 156 157 If you set this option to <literal>false</literal> and NixOS subsequently fails to 158 boot because it cannot import the root pool, you should boot with the 159 <literal>zfs_force=1</literal> option as a kernel parameter (e.g. by manually 160 editing the kernel params in grub during boot). You should only need to do this 161 once. 162 ''; 163 }; 164 165 forceImportAll = mkOption { 166 type = types.bool; 167 default = true; 168 description = '' 169 Forcibly import all ZFS pool(s). 170 171 This is enabled by default for backwards compatibility purposes, but it is highly 172 recommended to disable this option, as it bypasses some of the safeguards ZFS uses 173 to protect your ZFS pools. 174 175 If you set this option to <literal>false</literal> and NixOS subsequently fails to 176 import your non-root ZFS pool(s), you should manually import each pool with 177 "zpool import -f &lt;pool-name&gt;", and then reboot. You should only need to do 178 this once. 179 ''; 180 }; 181 182 requestEncryptionCredentials = mkOption { 183 type = types.bool; 184 default = config.boot.zfs.enableUnstable; 185 description = '' 186 Request encryption keys or passwords for all encrypted datasets on import. 187 Dataset encryption is only supported in zfsUnstable at the moment. 188 For root pools the encryption key can be supplied via both an 189 interactive prompt (keylocation=prompt) and from a file 190 (keylocation=file://). Note that for data pools the encryption key can 191 be only loaded from a file and not via interactive prompt since the 192 import is processed in a background systemd service. 193 ''; 194 }; 195 196 }; 197 198 services.zfs.autoSnapshot = { 199 enable = mkOption { 200 default = false; 201 type = types.bool; 202 description = '' 203 Enable the (OpenSolaris-compatible) ZFS auto-snapshotting service. 204 Note that you must set the <literal>com.sun:auto-snapshot</literal> 205 property to <literal>true</literal> on all datasets which you wish 206 to auto-snapshot. 207 208 You can override a child dataset to use, or not use auto-snapshotting 209 by setting its flag with the given interval: 210 <literal>zfs set com.sun:auto-snapshot:weekly=false DATASET</literal> 211 ''; 212 }; 213 214 flags = mkOption { 215 default = "-k -p"; 216 example = "-k -p --utc"; 217 type = types.str; 218 description = '' 219 Flags to pass to the zfs-auto-snapshot command. 220 221 Run <literal>zfs-auto-snapshot</literal> (without any arguments) to 222 see available flags. 223 224 If it's not too inconvenient for snapshots to have timestamps in UTC, 225 it is suggested that you append <literal>--utc</literal> to the list 226 of default options (see example). 227 228 Otherwise, snapshot names can cause name conflicts or apparent time 229 reversals due to daylight savings, timezone or other date/time changes. 230 ''; 231 }; 232 233 frequent = mkOption { 234 default = 4; 235 type = types.int; 236 description = '' 237 Number of frequent (15-minute) auto-snapshots that you wish to keep. 238 ''; 239 }; 240 241 hourly = mkOption { 242 default = 24; 243 type = types.int; 244 description = '' 245 Number of hourly auto-snapshots that you wish to keep. 246 ''; 247 }; 248 249 daily = mkOption { 250 default = 7; 251 type = types.int; 252 description = '' 253 Number of daily auto-snapshots that you wish to keep. 254 ''; 255 }; 256 257 weekly = mkOption { 258 default = 4; 259 type = types.int; 260 description = '' 261 Number of weekly auto-snapshots that you wish to keep. 262 ''; 263 }; 264 265 monthly = mkOption { 266 default = 12; 267 type = types.int; 268 description = '' 269 Number of monthly auto-snapshots that you wish to keep. 270 ''; 271 }; 272 }; 273 274 services.zfs.autoScrub = { 275 enable = mkOption { 276 default = false; 277 type = types.bool; 278 description = '' 279 Enables periodic scrubbing of ZFS pools. 280 ''; 281 }; 282 283 interval = mkOption { 284 default = "Sun, 02:00"; 285 type = types.str; 286 example = "daily"; 287 description = '' 288 Systemd calendar expression when to scrub ZFS pools. See 289 <citerefentry><refentrytitle>systemd.time</refentrytitle> 290 <manvolnum>7</manvolnum></citerefentry>. 291 ''; 292 }; 293 294 pools = mkOption { 295 default = []; 296 type = types.listOf types.str; 297 example = [ "tank" ]; 298 description = '' 299 List of ZFS pools to periodically scrub. If empty, all pools 300 will be scrubbed. 301 ''; 302 }; 303 }; 304 }; 305 306 ###### implementation 307 308 config = mkMerge [ 309 (mkIf enableZfs { 310 assertions = [ 311 { 312 assertion = config.networking.hostId != null; 313 message = "ZFS requires networking.hostId to be set"; 314 } 315 { 316 assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot; 317 message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot"; 318 } 319 { 320 assertion = cfgZfs.requestEncryptionCredentials -> cfgZfs.enableUnstable; 321 message = "This feature is only available for zfs unstable. Set the NixOS option boot.zfs.enableUnstable."; 322 } 323 ]; 324 325 virtualisation.lxd.zfsSupport = true; 326 327 boot = { 328 kernelModules = [ "zfs" ] ++ optional (!cfgZfs.enableUnstable) "spl"; 329 extraModulePackages = with packages; [ zfs ] ++ optional (!cfgZfs.enableUnstable) spl; 330 }; 331 332 boot.initrd = mkIf inInitrd { 333 kernelModules = [ "zfs" ] ++ optional (!cfgZfs.enableUnstable) "spl"; 334 extraUtilsCommands = 335 '' 336 copy_bin_and_libs ${packages.zfsUser}/sbin/zfs 337 copy_bin_and_libs ${packages.zfsUser}/sbin/zdb 338 copy_bin_and_libs ${packages.zfsUser}/sbin/zpool 339 ''; 340 extraUtilsCommandsTest = mkIf inInitrd 341 '' 342 $out/bin/zfs --help >/dev/null 2>&1 343 $out/bin/zpool --help >/dev/null 2>&1 344 ''; 345 postDeviceCommands = concatStringsSep "\n" (['' 346 ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}" 347 348 for o in $(cat /proc/cmdline); do 349 case $o in 350 zfs_force|zfs_force=1) 351 ZFS_FORCE="-f" 352 ;; 353 esac 354 done 355 ''] ++ [(importLib { 356 # See comments at importLib definition. 357 zpoolCmd = "zpool"; 358 awkCmd = "awk"; 359 inherit cfgZfs; 360 })] ++ (map (pool: '' 361 echo -n "importing root ZFS pool \"${pool}\"..." 362 # Loop across the import until it succeeds, because the devices needed may not be discovered yet. 363 if ! poolImported "${pool}"; then 364 for trial in `seq 1 60`; do 365 poolReady "${pool}" > /dev/null && msg="$(poolImport "${pool}" 2>&1)" && break 366 sleep 1 367 echo -n . 368 done 369 echo 370 if [[ -n "$msg" ]]; then 371 echo "$msg"; 372 fi 373 poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. 374 fi 375 ${lib.optionalString cfgZfs.requestEncryptionCredentials '' 376 zfs load-key -a 377 ''} 378 '') rootPools)); 379 }; 380 381 boot.loader.grub = mkIf inInitrd { 382 zfsSupport = true; 383 }; 384 385 environment.etc."zfs/zed.d".source = "${packages.zfsUser}/etc/zfs/zed.d/"; 386 387 system.fsPackages = [ packages.zfsUser ]; # XXX: needed? zfs doesn't have (need) a fsck 388 environment.systemPackages = [ packages.zfsUser ] 389 ++ optional enableAutoSnapshots autosnapPkg; # so the user can run the command to see flags 390 391 services.udev.packages = [ packages.zfsUser ]; # to hook zvol naming, etc. 392 systemd.packages = [ packages.zfsUser ]; 393 394 systemd.services = let 395 getPoolFilesystems = pool: 396 filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; 397 398 getPoolMounts = pool: 399 let 400 mountPoint = fs: escapeSystemdPath fs.mountPoint; 401 in 402 map (x: "${mountPoint x}.mount") (getPoolFilesystems pool); 403 404 createImportService = pool: 405 nameValuePair "zfs-import-${pool}" { 406 description = "Import ZFS pool \"${pool}\""; 407 requires = [ "systemd-udev-settle.service" ]; 408 after = [ "systemd-udev-settle.service" "systemd-modules-load.service" ]; 409 wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ]; 410 before = (getPoolMounts pool) ++ [ "local-fs.target" ]; 411 unitConfig = { 412 DefaultDependencies = "no"; 413 }; 414 serviceConfig = { 415 Type = "oneshot"; 416 RemainAfterExit = true; 417 }; 418 script = (importLib { 419 # See comments at importLib definition. 420 zpoolCmd="${packages.zfsUser}/sbin/zpool"; 421 awkCmd="${pkgs.gawk}/bin/awk"; 422 inherit cfgZfs; 423 }) + '' 424 poolImported "${pool}" && exit 425 echo -n "importing ZFS pool \"${pool}\"..." 426 # Loop across the import until it succeeds, because the devices needed may not be discovered yet. 427 for trial in `seq 1 60`; do 428 poolReady "${pool}" && poolImport "${pool}" && break 429 sleep 1 430 done 431 poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. 432 if poolImported "${pool}"; then 433 ${optionalString cfgZfs.requestEncryptionCredentials "\"${packages.zfsUser}/sbin/zfs\" load-key -r \"${pool}\""} 434 echo "Successfully imported ${pool}" 435 else 436 exit 1 437 fi 438 ''; 439 }; 440 441 # This forces a sync of any ZFS pools prior to poweroff, even if they're set 442 # to sync=disabled. 443 createSyncService = pool: 444 nameValuePair "zfs-sync-${pool}" { 445 description = "Sync ZFS pool \"${pool}\""; 446 wantedBy = [ "shutdown.target" ]; 447 unitConfig = { 448 DefaultDependencies = false; 449 }; 450 serviceConfig = { 451 Type = "oneshot"; 452 RemainAfterExit = true; 453 }; 454 script = '' 455 ${packages.zfsUser}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}" 456 ''; 457 }; 458 createZfsService = serv: 459 nameValuePair serv { 460 after = [ "systemd-modules-load.service" ]; 461 wantedBy = [ "zfs.target" ]; 462 }; 463 464 in listToAttrs (map createImportService dataPools ++ 465 map createSyncService allPools ++ 466 map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]); 467 468 systemd.targets."zfs-import" = 469 let 470 services = map (pool: "zfs-import-${pool}.service") dataPools; 471 in 472 { 473 requires = services; 474 after = services; 475 wantedBy = [ "zfs.target" ]; 476 }; 477 478 systemd.targets."zfs".wantedBy = [ "multi-user.target" ]; 479 }) 480 481 (mkIf enableAutoSnapshots { 482 systemd.services = let 483 descr = name: if name == "frequent" then "15 mins" 484 else if name == "hourly" then "hour" 485 else if name == "daily" then "day" 486 else if name == "weekly" then "week" 487 else if name == "monthly" then "month" 488 else throw "unknown snapshot name"; 489 numSnapshots = name: builtins.getAttr name cfgSnapshots; 490 in builtins.listToAttrs (map (snapName: 491 { 492 name = "zfs-snapshot-${snapName}"; 493 value = { 494 description = "ZFS auto-snapshotting every ${descr snapName}"; 495 after = [ "zfs-import.target" ]; 496 serviceConfig = { 497 Type = "oneshot"; 498 ExecStart = "${zfsAutoSnap} ${cfgSnapFlags} ${snapName} ${toString (numSnapshots snapName)}"; 499 }; 500 restartIfChanged = false; 501 }; 502 }) snapshotNames); 503 504 systemd.timers = let 505 timer = name: if name == "frequent" then "*:0,15,30,45" else name; 506 in builtins.listToAttrs (map (snapName: 507 { 508 name = "zfs-snapshot-${snapName}"; 509 value = { 510 wantedBy = [ "timers.target" ]; 511 timerConfig = { 512 OnCalendar = timer snapName; 513 Persistent = "yes"; 514 }; 515 }; 516 }) snapshotNames); 517 }) 518 519 (mkIf enableAutoScrub { 520 systemd.services.zfs-scrub = { 521 description = "ZFS pools scrubbing"; 522 after = [ "zfs-import.target" ]; 523 serviceConfig = { 524 Type = "oneshot"; 525 }; 526 script = '' 527 ${packages.zfsUser}/bin/zpool scrub ${ 528 if cfgScrub.pools != [] then 529 (concatStringsSep " " cfgScrub.pools) 530 else 531 "$(${packages.zfsUser}/bin/zpool list -H -o name)" 532 } 533 ''; 534 }; 535 536 systemd.timers.zfs-scrub = { 537 wantedBy = [ "timers.target" ]; 538 timerConfig = { 539 OnCalendar = cfgScrub.interval; 540 Persistent = "yes"; 541 }; 542 }; 543 }) 544 ]; 545}