1 { config, lib, pkgs, ... }:
6 cfg = config.services.sanoid;
8 datasetSettingsType = with types;
9 (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
10 description = "dataset/template options";
15 description = lib.mdDoc "Number of hourly snapshots.";
16 type = with types; nullOr ints.unsigned;
21 description = lib.mdDoc "Number of daily snapshots.";
22 type = with types; nullOr ints.unsigned;
27 description = lib.mdDoc "Number of monthly snapshots.";
28 type = with types; nullOr ints.unsigned;
33 description = lib.mdDoc "Number of yearly snapshots.";
34 type = with types; nullOr ints.unsigned;
38 autoprune = mkOption {
39 description = lib.mdDoc "Whether to automatically prune old snapshots.";
40 type = with types; nullOr bool;
45 description = lib.mdDoc "Whether to automatically take snapshots.";
46 type = with types; nullOr bool;
51 datasetOptions = rec {
52 use_template = mkOption {
53 description = lib.mdDoc "Names of the templates to use for this dataset.";
54 type = types.listOf (types.str // {
55 check = (types.enum (attrNames cfg.templates)).check;
56 description = "configured template name";
60 useTemplate = use_template;
62 recursive = mkOption {
63 description = lib.mdDoc ''
64 Whether to recursively snapshot dataset children.
65 You can also set this to `"zfs"` to handle datasets
66 recursively in an atomic way without the possibility to
67 override settings for child datasets.
69 type = with types; oneOf [ bool (enum [ "zfs" ]) ];
73 process_children_only = mkOption {
74 description = lib.mdDoc "Whether to only snapshot child datasets if recursing.";
78 processChildrenOnly = process_children_only;
81 # Extract unique dataset names
82 datasets = unique (attrNames cfg.datasets);
84 # Function to build "zfs allow" and "zfs unallow" commands for the
85 # filesystems we've delegated permissions to.
86 buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [
87 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
88 "-+/run/booted-system/sw/bin/zfs"
91 (concatStringsSep "," permissions)
98 if builtins.isList v then concatStringsSep "," v
99 else generators.mkValueStringDefault { } v;
103 else if k == "processChildrenOnly" then ""
104 else if k == "useTemplate" then ""
105 else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
107 generators.toINI { inherit mkKeyValue; } cfg.settings;
114 options.services.sanoid = {
115 enable = mkEnableOption (lib.mdDoc "Sanoid ZFS snapshotting service");
117 interval = mkOption {
121 description = lib.mdDoc ''
122 Run sanoid at this interval. The default is to run hourly.
124 The format is described in
125 {manpage}`systemd.time(7)`.
129 datasets = mkOption {
130 type = types.attrsOf (types.submodule ({ config, options, ... }: {
131 freeformType = datasetSettingsType;
132 options = commonOptions // datasetOptions;
133 config.use_template = modules.mkAliasAndWrapDefsWithPriority id (options.useTemplate or { });
134 config.process_children_only = modules.mkAliasAndWrapDefsWithPriority id (options.processChildrenOnly or { });
137 description = lib.mdDoc "Datasets to snapshot.";
140 templates = mkOption {
141 type = types.attrsOf (types.submodule {
142 freeformType = datasetSettingsType;
143 options = commonOptions;
146 description = lib.mdDoc "Templates for datasets.";
149 settings = mkOption {
150 type = types.attrsOf datasetSettingsType;
151 description = lib.mdDoc ''
152 Free-form settings written directly to the config file. See
153 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf>
158 extraArgs = mkOption {
159 type = types.listOf types.str;
161 example = [ "--verbose" "--readonly" "--debug" ];
162 description = lib.mdDoc ''
163 Extra arguments to pass to sanoid. See
164 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options>
172 config = mkIf cfg.enable {
173 services.sanoid.settings = mkMerge [
174 (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
175 (mapAttrs (d: v: v) cfg.datasets)
178 systemd.services.sanoid = {
179 description = "Sanoid snapshot service";
181 ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
182 ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
183 ExecStart = lib.escapeShellArgs ([
184 "${pkgs.sanoid}/bin/sanoid"
187 (pkgs.writeTextDir "sanoid.conf" configFile)
192 RuntimeDirectory = "sanoid";
193 CacheDirectory = "sanoid";
195 # Prevents missing snapshots during DST changes
196 environment.TZ = "UTC";
197 after = [ "zfs.target" ];
198 startAt = cfg.interval;
202 meta.maintainers = with maintainers; [ lopsided98 ];