summary refs log tree commit diff
path: root/nixos/modules/tasks/filesystems/zfs.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/tasks/filesystems/zfs.nix')
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix239
1 files changed, 172 insertions, 67 deletions
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 1c4bbc16b49..d4b10e9ed09 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -1,11 +1,10 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 #
 # todo:
 #   - crontab for scrubs, etc
 #   - zfs tunables
-#   - /etc/zfs/zpool.cache handling
-
 
+with utils;
 with lib;
 
 let
@@ -22,15 +21,30 @@ let
 
   kernel = config.boot.kernelPackages;
 
-  splPkg = if cfgZfs.useGit then kernel.spl_git else kernel.spl;
-  zfsPkg = if cfgZfs.useGit then kernel.zfs_git else kernel.zfs;
+  splKernelPkg = if cfgZfs.useGit then kernel.spl_git else kernel.spl;
+  zfsKernelPkg = if cfgZfs.useGit then kernel.zfs_git else kernel.zfs;
+  zfsUserPkg = if cfgZfs.useGit then pkgs.zfs_git else pkgs.zfs;
 
   autosnapPkg = pkgs.zfstools.override {
-    zfs = zfsPkg;
+    zfs = zfsUserPkg;
   };
 
   zfsAutoSnap = "${autosnapPkg}/bin/zfs-auto-snapshot";
 
+  datasetToPool = x: elemAt (splitString "/" x) 0;
+
+  fsToPool = fs: datasetToPool fs.device;
+
+  zfsFilesystems = filter (x: x.fsType == "zfs") (attrValues config.fileSystems);
+
+  isRoot = fs: fs.neededForBoot || elem fs.mountPoint [ "/" "/nix" "/nix/store" "/var" "/var/log" "/var/lib" "/etc" ];
+
+  allPools = unique ((map fsToPool zfsFilesystems) ++ cfgZfs.extraPools);
+
+  rootPools = unique (map fsToPool (filter isRoot zfsFilesystems));
+
+  dataPools = unique (filter (pool: !(elem pool rootPools)) allPools);
+
 in
 
 {
@@ -38,28 +52,73 @@ in
   ###### interface
 
   options = {
-    boot.spl.hostid = mkOption {
-      default = "";
-      example = "0xdeadbeef";
-      description = ''
-        ZFS uses a system's hostid to determine if a storage pool (zpool) is
-        native to this system, and should thus be imported automatically.
-        Unfortunately, this hostid can change under linux from boot to boot (by
-        changing network adapters, for instance). Specify a unique 32 bit hostid in
-        hex here for zfs to prevent getting a random hostid between boots and having to
-        manually import pools.
-      '';
-    };
+    boot.zfs = {
+      useGit = mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description = ''
+          Use the git version of the SPL and ZFS packages.
+          Note that these are unreleased versions, with less testing, and therefore
+          may be more unstable.
+        '';
+      };
+
+      extraPools = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "tank" "data" ];
+        description = ''
+          Name or GUID of extra ZFS pools that you wish to import during boot.
+
+          Usually this is not necessary. Instead, you should set the mountpoint property
+          of ZFS filesystems to <literal>legacy</literal> and add the ZFS filesystems to
+          NixOS's <option>fileSystems</option> option, which makes NixOS automatically
+          import the associated pool.
+
+          However, in some cases (e.g. if you have many filesystems) it may be preferable
+          to exclusively use ZFS commands to manage filesystems. If so, since NixOS/systemd
+          will not be managing those filesystems, you will need to specify the ZFS pool here
+          so that NixOS automatically imports it on every boot.
+        '';
+      };
+
+      forceImportRoot = mkOption {
+        type = types.bool;
+        default = true;
+        example = false;
+        description = ''
+          Forcibly import the ZFS root pool(s) during early boot.
+
+          This is enabled by default for backwards compatibility purposes, but it is highly
+          recommended to disable this option, as it bypasses some of the safeguards ZFS uses
+          to protect your ZFS pools.
+
+          If you set this option to <literal>false</literal> and NixOS subsequently fails to
+          boot because it cannot import the root pool, you should boot with the
+          <literal>zfs_force=1</literal> option as a kernel parameter (e.g. by manually
+          editing the kernel params in grub during boot). You should only need to do this
+          once.
+        '';
+      };
 
-    boot.zfs.useGit = mkOption {
-      type = types.bool;
-      default = false;
-      example = true;
-      description = ''
-        Use the git version of the SPL and ZFS packages.
-        Note that these are unreleased versions, with less testing, and therefore
-        may be more unstable.
-      '';
+      forceImportAll = mkOption {
+        type = types.bool;
+        default = true;
+        example = false;
+        description = ''
+          Forcibly import all ZFS pool(s).
+
+          This is enabled by default for backwards compatibility purposes, but it is highly
+          recommended to disable this option, as it bypasses some of the safeguards ZFS uses
+          to protect your ZFS pools.
+
+          If you set this option to <literal>false</literal> and NixOS subsequently fails to
+          import your non-root ZFS pool(s), you should manually import each pool with
+          "zpool import -f &lt;pool-name&gt;", and then reboot. You should only need to do
+          this once.
+        '';
+      };
     };
 
     services.zfs.autoSnapshot = {
@@ -124,67 +183,113 @@ in
 
   config = mkMerge [
     (mkIf enableZfs {
+      assertions = [
+        {
+          assertion = config.networking.hostId != null;
+          message = "ZFS requires config.networking.hostId to be set";
+        }
+        {
+          assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot;
+          message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot";
+        }
+      ];
+
       boot = {
         kernelModules = [ "spl" "zfs" ] ;
-        extraModulePackages = [ splPkg zfsPkg ];
-        extraModprobeConfig = mkIf (cfgSpl.hostid != "") ''
-          options spl spl_hostid=${cfgSpl.hostid}
-        '';
+        extraModulePackages = [ splKernelPkg zfsKernelPkg ];
       };
 
       boot.initrd = mkIf inInitrd {
         kernelModules = [ "spl" "zfs" ];
         extraUtilsCommands =
           ''
-            cp -v ${zfsPkg}/sbin/zfs $out/bin
-            cp -v ${zfsPkg}/sbin/zdb $out/bin
-            cp -v ${zfsPkg}/sbin/zpool $out/bin
-            cp -pdv ${zfsPkg}/lib/lib*.so* $out/lib
-            cp -pdv ${pkgs.zlib}/lib/lib*.so* $out/lib
+            copy_bin_and_libs ${zfsUserPkg}/sbin/zfs
+            copy_bin_and_libs ${zfsUserPkg}/sbin/zdb
+            copy_bin_and_libs ${zfsUserPkg}/sbin/zpool
           '';
-        postDeviceCommands =
+        extraUtilsCommandsTest = mkIf inInitrd
           ''
-            zpool import -f -a
+            $out/bin/zfs --help >/dev/null 2>&1
+            $out/bin/zpool --help >/dev/null 2>&1
           '';
+        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
+            ''] ++ (map (pool: ''
+            echo "importing root ZFS pool \"${pool}\"..."
+            zpool import -N $ZFS_FORCE "${pool}"
+        '') rootPools));
       };
 
       boot.loader.grub = mkIf inInitrd {
         zfsSupport = true;
       };
 
-      systemd.services."zpool-import" = {
-        description = "Import zpools";
-        after = [ "systemd-udev-settle.service" ];
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-          ExecStart = "${zfsPkg}/sbin/zpool import -f -a";
-        };
-        restartIfChanged = false;
+      environment.etc."zfs/zed.d".source = "${zfsUserPkg}/etc/zfs/zed.d/*";
+
+      system.fsPackages = [ zfsUserPkg ];                  # XXX: needed? zfs doesn't have (need) a fsck
+      environment.systemPackages = [ zfsUserPkg ];
+      services.udev.packages = [ zfsUserPkg ];             # to hook zvol naming, etc.
+      systemd.packages = [ zfsUserPkg ];
+
+      systemd.services = let
+        getPoolFilesystems = pool:
+          filter (x: x.fsType == "zfs" && (fsToPool x) == pool) (attrValues config.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}\"";
+            requires = [ "systemd-udev-settle.service" ];
+            after = [ "systemd-udev-settle.service" "systemd-modules-load.service" ];
+            wantedBy = (getPoolMounts pool) ++ [ "local-fs.target" ];
+            before = (getPoolMounts pool) ++ [ "local-fs.target" ];
+            unitConfig = {
+              DefaultDependencies = "no";
+            };
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+            script = ''
+              zpool_cmd="${zfsUserPkg}/sbin/zpool"
+              ("$zpool_cmd" list "${pool}" >/dev/null) || "$zpool_cmd" import -N ${optionalString cfgZfs.forceImportAll "-f"} "${pool}"
+            '';
+          };
+      in listToAttrs (map createImportService dataPools) // {
+        "zfs-mount" = { after = [ "systemd-modules-load.service" ]; };
+        "zfs-share" = { after = [ "systemd-modules-load.service" ]; };
+        "zed" = { after = [ "systemd-modules-load.service" ]; };
       };
 
-      systemd.services."zfs-mount" = {
-        description = "Mount ZFS Volumes";
-        after = [ "zpool-import.service" ];
-        wantedBy = [ "local-fs.target" ];
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-          ExecStart = "${zfsPkg}/sbin/zfs mount -a";
-          ExecStop = "${zfsPkg}/sbin/zfs umount -a";
-        };
-        restartIfChanged = false;
-      };
+      systemd.targets."zfs-import" =
+        let
+          services = map (pool: "zfs-import-${pool}.service") dataPools;
+        in
+          {
+            requires = services;
+            after = services;
+          };
 
-      system.fsPackages = [ zfsPkg ];                  # XXX: needed? zfs doesn't have (need) a fsck
-      environment.systemPackages = [ zfsPkg ];
-      services.udev.packages = [ zfsPkg ];             # to hook zvol naming, etc.
+      systemd.targets."zfs".wantedBy = [ "multi-user.target" ];
     })
 
     (mkIf enableAutoSnapshots {
       systemd.services."zfs-snapshot-frequent" = {
         description = "ZFS auto-snapshotting every 15 mins";
-        after = [ "zpool-import.service" ];
+        after = [ "zfs-import.target" ];
         serviceConfig = {
           Type = "oneshot";
           ExecStart = "${zfsAutoSnap} frequent ${toString cfgSnapshots.frequent}";
@@ -195,7 +300,7 @@ in
 
       systemd.services."zfs-snapshot-hourly" = {
         description = "ZFS auto-snapshotting every hour";
-        after = [ "zpool-import.service" ];
+        after = [ "zfs-import.target" ];
         serviceConfig = {
           Type = "oneshot";
           ExecStart = "${zfsAutoSnap} hourly ${toString cfgSnapshots.hourly}";
@@ -206,7 +311,7 @@ in
 
       systemd.services."zfs-snapshot-daily" = {
         description = "ZFS auto-snapshotting every day";
-        after = [ "zpool-import.service" ];
+        after = [ "zfs-import.target" ];
         serviceConfig = {
           Type = "oneshot";
           ExecStart = "${zfsAutoSnap} daily ${toString cfgSnapshots.daily}";
@@ -217,7 +322,7 @@ in
 
       systemd.services."zfs-snapshot-weekly" = {
         description = "ZFS auto-snapshotting every week";
-        after = [ "zpool-import.service" ];
+        after = [ "zfs-import.target" ];
         serviceConfig = {
           Type = "oneshot";
           ExecStart = "${zfsAutoSnap} weekly ${toString cfgSnapshots.weekly}";
@@ -228,7 +333,7 @@ in
 
       systemd.services."zfs-snapshot-monthly" = {
         description = "ZFS auto-snapshotting every month";
-        after = [ "zpool-import.service" ];
+        after = [ "zfs-import.target" ];
         serviceConfig = {
           Type = "oneshot";
           ExecStart = "${zfsAutoSnap} monthly ${toString cfgSnapshots.monthly}";