diff options
Diffstat (limited to 'nixos/modules/tasks/filesystems/zfs.nix')
-rw-r--r-- | nixos/modules/tasks/filesystems/zfs.nix | 115 |
1 files changed, 94 insertions, 21 deletions
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix index c3bf897d51fd..7120856387ef 100644 --- a/nixos/modules/tasks/filesystems/zfs.nix +++ b/nixos/modules/tasks/filesystems/zfs.nix @@ -9,7 +9,6 @@ with lib; let - cfgSpl = config.boot.spl; cfgZfs = config.boot.zfs; cfgSnapshots = config.services.zfs.autoSnapshot; cfgSnapFlags = cfgSnapshots.flags; @@ -58,6 +57,45 @@ let snapshotNames = [ "frequent" "hourly" "daily" "weekly" "monthly" ]; + # When importing ZFS pools, there's one difficulty: These scripts may run + # before the backing devices (physical HDDs, etc.) of the pool have been + # scanned and initialized. + # + # An attempted import with all devices missing will just fail, and can be + # retried, but an import where e.g. two out of three disks in a three-way + # mirror are missing, will succeed. This is a problem: When the missing disks + # are later discovered, they won't be automatically set online, rendering the + # pool redundancy-less (and far slower) until such time as the system reboots. + # + # The solution is the below. poolReady checks the status of an un-imported + # pool, to see if *every* device is available -- in which case the pool will be + # in state ONLINE, as opposed to DEGRADED, FAULTED or MISSING. + # + # The import scripts then loop over this, waiting until the pool is ready or a + # sufficient amount of time has passed that we can assume it won't be. In the + # latter case it makes one last attempt at importing, allowing the system to + # (eventually) boot even with a degraded pool. + importLib = {zpoolCmd, awkCmd, cfgZfs}: '' + poolReady() { + pool="$1" + state="$("${zpoolCmd}" import | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")" + if [[ "$state" = "ONLINE" ]]; then + return 0 + else + echo "Pool $pool in state $state, waiting" + return 1 + fi + } + poolImported() { + pool="$1" + "${zpoolCmd}" list "$pool" >/dev/null 2>/dev/null + } + poolImport() { + pool="$1" + "${zpoolCmd}" import -d "${cfgZfs.devNodes}" -N $ZFS_FORCE "$pool" + } + ''; + in { @@ -171,8 +209,12 @@ in default = config.boot.zfs.enableUnstable; description = '' Request encryption keys or passwords for all encrypted datasets on import. - Dataset encryption is only supported in zfsUnstable at the moment. + For root pools the encryption key can be supplied via both an + interactive prompt (keylocation=prompt) and from a file + (keylocation=file://). Note that for data pools the encryption key can + be only loaded from a file and not via interactive prompt since the + import is processed in a background systemd service. ''; }; @@ -335,19 +377,26 @@ in ;; esac done - ''] ++ (map (pool: '' + ''] ++ [(importLib { + # See comments at importLib definition. + zpoolCmd = "zpool"; + awkCmd = "awk"; + inherit cfgZfs; + })] ++ (map (pool: '' echo -n "importing root ZFS pool \"${pool}\"..." - trial=0 - until msg="$(zpool import -d ${cfgZfs.devNodes} -N $ZFS_FORCE '${pool}' 2>&1)"; do - sleep 0.25 - echo -n . - trial=$(($trial + 1)) - if [[ $trial -eq 60 ]]; then - break + # Loop across the import until it succeeds, because the devices needed may not be discovered yet. + if ! poolImported "${pool}"; then + for trial in `seq 1 60`; do + poolReady "${pool}" > /dev/null && msg="$(poolImport "${pool}" 2>&1)" && break + sleep 1 + echo -n . + done + echo + if [[ -n "$msg" ]]; then + echo "$msg"; fi - done - echo - if [[ -n "$msg" ]]; then echo "$msg"; fi + poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. + fi ${lib.optionalString cfgZfs.requestEncryptionCredentials '' zfs load-key -a ''} @@ -391,9 +440,26 @@ in Type = "oneshot"; RemainAfterExit = true; }; - script = '' - zpool_cmd="${packages.zfsUser}/sbin/zpool" - ("$zpool_cmd" list "${pool}" >/dev/null) || "$zpool_cmd" import -d ${cfgZfs.devNodes} -N ${optionalString cfgZfs.forceImportAll "-f"} "${pool}" + script = (importLib { + # See comments at importLib definition. + zpoolCmd="${packages.zfsUser}/sbin/zpool"; + awkCmd="${pkgs.gawk}/bin/awk"; + inherit cfgZfs; + }) + '' + poolImported "${pool}" && exit + echo -n "importing ZFS pool \"${pool}\"..." + # Loop across the import until it succeeds, because the devices needed may not be discovered yet. + for trial in `seq 1 60`; do + poolReady "${pool}" && poolImport "${pool}" && break + sleep 1 + done + poolImported "${pool}" || poolImport "${pool}" # Try one last time, e.g. to import a degraded pool. + if poolImported "${pool}"; then + ${optionalString cfgZfs.requestEncryptionCredentials "\"${packages.zfsUser}/sbin/zfs\" load-key -r \"${pool}\""} + echo "Successfully imported ${pool}" + else + exit 1 + fi ''; }; @@ -403,6 +469,9 @@ in nameValuePair "zfs-sync-${pool}" { description = "Sync ZFS pool \"${pool}\""; wantedBy = [ "shutdown.target" ]; + unitConfig = { + DefaultDependencies = false; + }; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; @@ -411,12 +480,15 @@ in ${packages.zfsUser}/sbin/zfs set nixos:shutdown-time="$(date)" "${pool}" ''; }; + createZfsService = serv: + nameValuePair serv { + after = [ "systemd-modules-load.service" ]; + wantedBy = [ "zfs.target" ]; + }; - in listToAttrs (map createImportService dataPools ++ map createSyncService allPools) // { - "zfs-mount" = { after = [ "systemd-modules-load.service" ]; }; - "zfs-share" = { after = [ "systemd-modules-load.service" ]; }; - "zfs-zed" = { after = [ "systemd-modules-load.service" ]; }; - }; + in listToAttrs (map createImportService dataPools ++ + map createSyncService allPools ++ + map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]); systemd.targets."zfs-import" = let @@ -425,6 +497,7 @@ in { requires = services; after = services; + wantedBy = [ "zfs.target" ]; }; systemd.targets."zfs".wantedBy = [ "multi-user.target" ]; |