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.nix204
1 files changed, 204 insertions, 0 deletions
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
new file mode 100644
index 00000000000..5eb031b2e9f
--- /dev/null
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -0,0 +1,204 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.sanoid;
+
+  datasetSettingsType = with types;
+    (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
+      description = "dataset/template options";
+    };
+
+  commonOptions = {
+    hourly = mkOption {
+      description = "Number of hourly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    daily = mkOption {
+      description = "Number of daily snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    monthly = mkOption {
+      description = "Number of monthly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    yearly = mkOption {
+      description = "Number of yearly snapshots.";
+      type = with types; nullOr ints.unsigned;
+      default = null;
+    };
+
+    autoprune = mkOption {
+      description = "Whether to automatically prune old snapshots.";
+      type = with types; nullOr bool;
+      default = null;
+    };
+
+    autosnap = mkOption {
+      description = "Whether to automatically take snapshots.";
+      type = with types; nullOr bool;
+      default = null;
+    };
+  };
+
+  datasetOptions = rec {
+    use_template = mkOption {
+      description = "Names of the templates to use for this dataset.";
+      type = types.listOf (types.str // {
+        check = (types.enum (attrNames cfg.templates)).check;
+        description = "configured template name";
+      });
+      default = [ ];
+    };
+    useTemplate = use_template;
+
+    recursive = mkOption {
+      description = ''
+        Whether to recursively snapshot dataset children.
+        You can also set this to <literal>"zfs"</literal> to handle datasets
+        recursively in an atomic way without the possibility to
+        override settings for child datasets.
+      '';
+      type = with types; oneOf [ bool (enum [ "zfs" ]) ];
+      default = false;
+    };
+
+    process_children_only = mkOption {
+      description = "Whether to only snapshot child datasets if recursing.";
+      type = types.bool;
+      default = false;
+    };
+    processChildrenOnly = process_children_only;
+  };
+
+  # 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>.
+      '';
+    };
+
+    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 {
+        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.
+      '';
+    };
+
+    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) 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 ];
+}