diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix index 5eca68798d5..c8bbfe9769b 100644 --- a/nixos/modules/tasks/filesystems/zfs.nix +++ b/nixos/modules/tasks/filesystems/zfs.nix @@ -58,6 +58,13 @@ let # latter case it makes one last attempt at importing, allowing the system to # (eventually) boot even with a degraded pool. importLib = {zpoolCmd, awkCmd, cfgZfs}: '' + for o in $(cat /proc/cmdline); do + case $o in + zfs_force|zfs_force=1|zfs_force=y) + ZFS_FORCE="-f" + ;; + esac + done poolReady() { pool="$1" state="$("${zpoolCmd}" import 2>/dev/null | "${awkCmd}" "/pool: $pool/ { found = 1 }; /state:/ { if (found == 1) { print \$2; exit } }; END { if (found == 0) { print \"MISSING\" } }")" @@ -78,6 +85,95 @@ let } ''; + getPoolFilesystems = pool: + filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; + + getPoolMounts = prefix: pool: + let + # Remove the "/" suffix because even though most mountpoints + # won't have it, the "/" mountpoint will, and we can't have the + # trailing slash in "/sysroot/" in stage 1. + mountPoint = fs: escapeSystemdPath (prefix + (lib.removeSuffix "/" fs.mountPoint)); + in + map (x: "${mountPoint x}.mount") (getPoolFilesystems pool); + + getKeyLocations = pool: + if isBool cfgZfs.requestEncryptionCredentials + then "${cfgZfs.package}/sbin/zfs list -rHo name,keylocation,keystatus ${pool}" + else "${cfgZfs.package}/sbin/zfs list -Ho name,keylocation,keystatus ${toString (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials)}"; + + createImportService = { pool, systemd, force, prefix ? "" }: + nameValuePair "zfs-import-${pool}" { + description = "Import ZFS pool \"${pool}\""; + # we need systemd-udev-settle to ensure devices are available + # In the future, hopefully someone will complete this: + # https://github.com/zfsonlinux/zfs/pull/4943 + requires = [ "systemd-udev-settle.service" ]; + after = [ + "systemd-udev-settle.service" + "systemd-modules-load.service" + "systemd-ask-password-console.service" + ]; + wantedBy = (getPoolMounts prefix pool) ++ [ "local-fs.target" ]; + before = (getPoolMounts prefix pool) ++ [ "local-fs.target" ]; + unitConfig = { + DefaultDependencies = "no"; + }; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + environment.ZFS_FORCE = optionalString force "-f"; + script = (importLib { + # See comments at importLib definition. + zpoolCmd = "${cfgZfs.package}/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 (if isBool cfgZfs.requestEncryptionCredentials + then cfgZfs.requestEncryptionCredentials + else cfgZfs.requestEncryptionCredentials != []) '' + ${getKeyLocations pool} | while IFS=$'\t' read ds kl ks; do + { + if [[ "$ks" != unavailable ]]; then + continue + fi + case "$kl" in + none ) + ;; + prompt ) + tries=3 + success=false + while [[ $success != true ]] && [[ $tries -gt 0 ]]; do + ${systemd}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \ + && success=true \ + || tries=$((tries - 1)) + done + [[ $success = true ]] + ;; + * ) + ${cfgZfs.package}/sbin/zfs load-key "$ds" + ;; + esac + } < /dev/null # To protect while read ds kl in case anything reads stdin + done + ''} + echo "Successfully imported ${pool}" + else + exit 1 + fi + ''; + }; + zedConf = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { mkValueString = v: @@ -428,14 +524,6 @@ in ''; postDeviceCommands = concatStringsSep "\n" (['' ZFS_FORCE="${optionalString cfgZfs.forceImportRoot "-f"}" - - for o in $(cat /proc/cmdline); do - case $o in - zfs_force|zfs_force=1) - ZFS_FORCE="-f" - ;; - esac - done ''] ++ [(importLib { # See comments at importLib definition. zpoolCmd = "zpool"; @@ -464,6 +552,21 @@ in zfs load-key ${fs} '') cfgZfs.requestEncryptionCredentials} '') rootPools)); + + # Systemd in stage 1 + systemd = { + packages = [cfgZfs.package]; + services = listToAttrs (map (pool: createImportService { + inherit pool; + systemd = config.boot.initrd.systemd.package; + force = cfgZfs.forceImportRoot; + prefix = "/sysroot"; + }) rootPools); + extraBin = { + # zpool and zfs are already in thanks to fsPackages + awk = "${pkgs.gawk}/bin/awk"; + }; + }; }; systemd.shutdownRamfs.contents."/etc/systemd/system-shutdown/zpool".source = pkgs.writeShellScript "zpool-sync-shutdown" '' @@ -521,79 +624,11 @@ in systemd.packages = [ cfgZfs.package ]; systemd.services = let - getPoolFilesystems = pool: - filter (x: x.fsType == "zfs" && (fsToPool x) == pool) config.system.build.fileSystems; - - getPoolMounts = pool: - let - mountPoint = fs: escapeSystemdPath fs.mountPoint; - in - map (x: "${mountPoint x}.mount") (getPoolFilesystems pool); - - createImportService = pool: - nameValuePair "zfs-import-${pool}" { - description = "Import ZFS pool \"${pool}\""; - # we need systemd-udev-settle until https://github.com/zfsonlinux/zfs/pull/4943 is merged - requires = [ "systemd-udev-settle.service" ]; - after = [ - "systemd-udev-settle.service" - "systemd-modules-load.service" - "systemd-ask-password-console.service" - ]; - wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ]; - before = (getPoolMounts pool) ++ [ "local-fs.target" ]; - unitConfig = { - DefaultDependencies = "no"; - }; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - environment.ZFS_FORCE = optionalString cfgZfs.forceImportAll "-f"; - script = (importLib { - # See comments at importLib definition. - zpoolCmd = "${cfgZfs.package}/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 (if isBool cfgZfs.requestEncryptionCredentials - then cfgZfs.requestEncryptionCredentials - else cfgZfs.requestEncryptionCredentials != []) '' - ${cfgZfs.package}/sbin/zfs list -rHo name,keylocation ${pool} | while IFS=$'\t' read ds kl; do - { - ${optionalString (!isBool cfgZfs.requestEncryptionCredentials) '' - if ! echo '${concatStringsSep "\n" cfgZfs.requestEncryptionCredentials}' | grep -qFx "$ds"; then - continue - fi - ''} - case "$kl" in - none ) - ;; - prompt ) - ${config.systemd.package}/bin/systemd-ask-password "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" - ;; - * ) - ${cfgZfs.package}/sbin/zfs load-key "$ds" - ;; - esac - } < /dev/null # To protect while read ds kl in case anything reads stdin - done - ''} - echo "Successfully imported ${pool}" - else - exit 1 - fi - ''; - }; + createImportService' = pool: createImportService { + inherit pool; + systemd = config.systemd.package; + force = cfgZfs.forceImportAll; + }; # This forces a sync of any ZFS pools prior to poweroff, even if they're set # to sync=disabled. @@ -619,7 +654,7 @@ in wantedBy = [ "zfs.target" ]; }; - in listToAttrs (map createImportService dataPools ++ + in listToAttrs (map createImportService' dataPools ++ map createSyncService allPools ++ map createZfsService [ "zfs-mount" "zfs-share" "zfs-zed" ]); diff --git a/nixos/tests/installer-systemd-stage-1.nix b/nixos/tests/installer-systemd-stage-1.nix index a8b418626e6..d02387ee80e 100644 --- a/nixos/tests/installer-systemd-stage-1.nix +++ b/nixos/tests/installer-systemd-stage-1.nix @@ -27,7 +27,7 @@ simpleUefiGrubSpecialisation simpleUefiSystemdBoot # swraid - # zfsroot + zfsroot ; }