summary refs log tree commit diff
path: root/nixos/modules/services/backup/sanoid.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/backup/sanoid.nix')
-rw-r--r--nixos/modules/services/backup/sanoid.nix283
1 files changed, 133 insertions, 150 deletions
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index 0472fb4ba1e..41d0e2e1df6 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -10,74 +10,51 @@ let
       description = "dataset/template options";
     };
 
-  # Default values from https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf
-
   commonOptions = {
     hourly = mkOption {
       description = "Number of hourly snapshots.";
-      type = types.ints.unsigned;
-      default = 48;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     daily = mkOption {
       description = "Number of daily snapshots.";
-      type = types.ints.unsigned;
-      default = 90;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     monthly = mkOption {
       description = "Number of monthly snapshots.";
-      type = types.ints.unsigned;
-      default = 6;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     yearly = mkOption {
       description = "Number of yearly snapshots.";
-      type = types.ints.unsigned;
-      default = 0;
+      type = with types; nullOr ints.unsigned;
+      default = null;
     };
 
     autoprune = mkOption {
       description = "Whether to automatically prune old snapshots.";
-      type = types.bool;
-      default = true;
+      type = with types; nullOr bool;
+      default = null;
     };
 
     autosnap = mkOption {
       description = "Whether to automatically take snapshots.";
-      type = types.bool;
-      default = true;
-    };
-
-    settings = mkOption {
-      description = ''
-        Free-form settings for this template/dataset. See
-        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
-        for allowed values.
-      '';
-      type = datasetSettingsType;
-    };
-  };
-
-  commonConfig = config: {
-    settings = {
-      hourly = mkDefault config.hourly;
-      daily = mkDefault config.daily;
-      monthly = mkDefault config.monthly;
-      yearly = mkDefault config.yearly;
-      autoprune = mkDefault config.autoprune;
-      autosnap = mkDefault config.autosnap;
+      type = with types; nullOr bool;
+      default = null;
     };
   };
 
-  datasetOptions = {
-    useTemplate = mkOption {
+  datasetOptions = rec {
+    use_template = mkOption {
       description = "Names of the templates to use for this dataset.";
-      type = (types.listOf (types.enum (attrNames cfg.templates))) // {
-        description = "list of template names";
-      };
-      default = [];
+      type = types.listOf (types.enum (attrNames cfg.templates));
+      default = [ ];
     };
+    useTemplate = use_template;
 
     recursive = mkOption {
       description = "Whether to recursively snapshot dataset children.";
@@ -85,129 +62,135 @@ let
       default = false;
     };
 
-    processChildrenOnly = mkOption {
+    process_children_only = mkOption {
       description = "Whether to only snapshot child datasets if recursing.";
       type = types.bool;
       default = false;
     };
+    processChildrenOnly = process_children_only;
   };
 
-  datasetConfig = config: {
-    settings = {
-      use_template = mkDefault config.useTemplate;
-      recursive = mkDefault config.recursive;
-      process_children_only = mkDefault config.processChildrenOnly;
-    };
-  };
-
-  # Extract pool names from configured datasets
-  pools = unique (map (d: head (builtins.match "([^/]+).*" d)) (attrNames cfg.datasets));
-
-  configFile = let
-    mkValueString = v:
-      if builtins.isList v then concatStringsSep "," v
-      else generators.mkValueStringDefault {} v;
-
-    mkKeyValue = k: v: if v == null then ""
-      else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
-  in generators.toINI { inherit mkKeyValue; } cfg.settings;
-
-  configDir = pkgs.writeTextDir "sanoid.conf" configFile;
-
-in {
-
-    # Interface
-
-    options.services.sanoid = {
-      enable = mkEnableOption "Sanoid ZFS snapshotting service";
-
-      interval = mkOption {
-        type = types.str;
-        default = "hourly";
-        example = "daily";
-        description = ''
-          Run sanoid at this interval. The default is to run hourly.
+  # Extract unique dataset names
+  datasets = unique (attrNames cfg.datasets);
+
+  # Function to build "zfs allow" and "zfs unallow" commands for the
+  # filesystems we've delegated permissions to.
+  buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
+    # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
+    "-+/run/booted-system/sw/bin/zfs"
+    zfsAction
+    "sanoid"
+    (concatStringsSep "," permissions)
+    dataset
+  ];
+
+  configFile =
+    let
+      mkValueString = v:
+        if builtins.isList v then concatStringsSep "," v
+        else generators.mkValueStringDefault { } v;
+
+      mkKeyValue = k: v:
+        if v == null then ""
+        else if k == "processChildrenOnly" then ""
+        else if k == "useTemplate" then ""
+        else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
+    in
+    generators.toINI { inherit mkKeyValue; } cfg.settings;
+
+in
+{
+
+  # Interface
+
+  options.services.sanoid = {
+    enable = mkEnableOption "Sanoid ZFS snapshotting service";
+
+    interval = mkOption {
+      type = types.str;
+      default = "hourly";
+      example = "daily";
+      description = ''
+        Run sanoid at this interval. The default is to run hourly.
 
-          The format is described in
-          <citerefentry><refentrytitle>systemd.time</refentrytitle>
-          <manvolnum>7</manvolnum></citerefentry>.
-        '';
-      };
+        The format is described in
+        <citerefentry><refentrytitle>systemd.time</refentrytitle>
+        <manvolnum>7</manvolnum></citerefentry>.
+      '';
+    };
 
-      datasets = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }: {
-          options = commonOptions // datasetOptions;
-          config = mkMerge [ (commonConfig config) (datasetConfig config) ];
-        }));
-        default = {};
-        description = "Datasets to snapshot.";
-      };
+    datasets = mkOption {
+      type = types.attrsOf (types.submodule ({ config, options, ... }: {
+        freeformType = datasetSettingsType;
+        options = commonOptions // datasetOptions;
+        config.use_template = mkAliasDefinitions (mkDefault options.useTemplate or { });
+        config.process_children_only = mkAliasDefinitions (mkDefault options.processChildrenOnly or { });
+      }));
+      default = { };
+      description = "Datasets to snapshot.";
+    };
 
-      templates = mkOption {
-        type = types.attrsOf (types.submodule ({ config, ... }: {
-          options = commonOptions;
-          config = commonConfig config;
-        }));
-        default = {};
-        description = "Templates for datasets.";
-      };
+    templates = mkOption {
+      type = types.attrsOf (types.submodule {
+        freeformType = datasetSettingsType;
+        options = commonOptions;
+      });
+      default = { };
+      description = "Templates for datasets.";
+    };
 
-      settings = mkOption {
-        type = types.attrsOf datasetSettingsType;
-        description = ''
-          Free-form settings written directly to the config file. See
-          <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
-          for allowed values.
-        '';
-      };
+    settings = mkOption {
+      type = types.attrsOf datasetSettingsType;
+      description = ''
+        Free-form settings written directly to the config file. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf"/>
+        for allowed values.
+      '';
+    };
 
-      extraArgs = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [ "--verbose" "--readonly" "--debug" ];
-        description = ''
-          Extra arguments to pass to sanoid. See
-          <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
-          for allowed options.
-        '';
-      };
+    extraArgs = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      example = [ "--verbose" "--readonly" "--debug" ];
+      description = ''
+        Extra arguments to pass to sanoid. See
+        <link xlink:href="https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options"/>
+        for allowed options.
+      '';
     };
+  };
 
-    # Implementation
-
-    config = mkIf cfg.enable {
-      services.sanoid.settings = mkMerge [
-        (mapAttrs' (d: v: nameValuePair ("template_" + d) v.settings) cfg.templates)
-        (mapAttrs (d: v: v.settings) cfg.datasets)
-      ];
-
-      systemd.services.sanoid = {
-        description = "Sanoid snapshot service";
-        serviceConfig = {
-          ExecStartPre = map (pool: lib.escapeShellArgs [
-            "+/run/booted-system/sw/bin/zfs" "allow"
-            "sanoid" "snapshot,mount,destroy" pool
-          ]) pools;
-          ExecStart = lib.escapeShellArgs ([
-            "${pkgs.sanoid}/bin/sanoid"
-            "--cron"
-            "--configdir" configDir
-          ] ++ cfg.extraArgs);
-          ExecStopPost = map (pool: lib.escapeShellArgs [
-            "+/run/booted-system/sw/bin/zfs" "unallow" "sanoid" pool
-          ]) pools;
-          User = "sanoid";
-          Group = "sanoid";
-          DynamicUser = true;
-          RuntimeDirectory = "sanoid";
-          CacheDirectory = "sanoid";
-        };
-        # Prevents missing snapshots during DST changes
-        environment.TZ = "UTC";
-        after = [ "zfs.target" ];
-        startAt = cfg.interval;
+  # Implementation
+
+  config = mkIf cfg.enable {
+    services.sanoid.settings = mkMerge [
+      (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
+      (mapAttrs (d: v: v) cfg.datasets)
+    ];
+
+    systemd.services.sanoid = {
+      description = "Sanoid snapshot service";
+      serviceConfig = {
+        ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
+        ExecStart = lib.escapeShellArgs ([
+          "${pkgs.sanoid}/bin/sanoid"
+          "--cron"
+          "--configdir"
+          (pkgs.writeTextDir "sanoid.conf" configFile)
+        ] ++ cfg.extraArgs);
+        User = "sanoid";
+        Group = "sanoid";
+        DynamicUser = true;
+        RuntimeDirectory = "sanoid";
+        CacheDirectory = "sanoid";
       };
+      # Prevents missing snapshots during DST changes
+      environment.TZ = "UTC";
+      after = [ "zfs.target" ];
+      startAt = cfg.interval;
     };
+  };
 
-    meta.maintainers = with maintainers; [ lopsided98 ];
-  }
+  meta.maintainers = with maintainers; [ lopsided98 ];
+}