1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.sanoid;
7
8 datasetSettingsType = with types;
9 (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // {
10 description = "dataset/template options";
11 };
12
13 commonOptions = {
14 hourly = mkOption {
15 description = lib.mdDoc "Number of hourly snapshots.";
16 type = with types; nullOr ints.unsigned;
17 default = null;
18 };
19
20 daily = mkOption {
21 description = lib.mdDoc "Number of daily snapshots.";
22 type = with types; nullOr ints.unsigned;
23 default = null;
24 };
25
26 monthly = mkOption {
27 description = lib.mdDoc "Number of monthly snapshots.";
28 type = with types; nullOr ints.unsigned;
29 default = null;
30 };
31
32 yearly = mkOption {
33 description = lib.mdDoc "Number of yearly snapshots.";
34 type = with types; nullOr ints.unsigned;
35 default = null;
36 };
37
38 autoprune = mkOption {
39 description = lib.mdDoc "Whether to automatically prune old snapshots.";
40 type = with types; nullOr bool;
41 default = null;
42 };
43
44 autosnap = mkOption {
45 description = lib.mdDoc "Whether to automatically take snapshots.";
46 type = with types; nullOr bool;
47 default = null;
48 };
49 };
50
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";
57 });
58 default = [ ];
59 };
60 useTemplate = use_template;
61
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.
68 '';
69 type = with types; oneOf [ bool (enum [ "zfs" ]) ];
70 default = false;
71 };
72
73 process_children_only = mkOption {
74 description = lib.mdDoc "Whether to only snapshot child datasets if recursing.";
75 type = types.bool;
76 default = false;
77 };
78 processChildrenOnly = process_children_only;
79 };
80
81 # Extract unique dataset names
82 datasets = unique (attrNames cfg.datasets);
83
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"
89 zfsAction
90 "sanoid"
91 (concatStringsSep "," permissions)
92 dataset
93 ];
94
95 configFile =
96 let
97 mkValueString = v:
98 if builtins.isList v then concatStringsSep "," v
99 else generators.mkValueStringDefault { } v;
100
101 mkKeyValue = k: v:
102 if v == null then ""
103 else if k == "processChildrenOnly" then ""
104 else if k == "useTemplate" then ""
105 else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
106 in
107 generators.toINI { inherit mkKeyValue; } cfg.settings;
108
109in
110{
111
112 # Interface
113
114 options.services.sanoid = {
115 enable = mkEnableOption (lib.mdDoc "Sanoid ZFS snapshotting service");
116
117 package = lib.mkPackageOptionMD pkgs "sanoid" {};
118
119 interval = mkOption {
120 type = types.str;
121 default = "hourly";
122 example = "daily";
123 description = lib.mdDoc ''
124 Run sanoid at this interval. The default is to run hourly.
125
126 The format is described in
127 {manpage}`systemd.time(7)`.
128 '';
129 };
130
131 datasets = mkOption {
132 type = types.attrsOf (types.submodule ({ config, options, ... }: {
133 freeformType = datasetSettingsType;
134 options = commonOptions // datasetOptions;
135 config.use_template = modules.mkAliasAndWrapDefsWithPriority id (options.useTemplate or { });
136 config.process_children_only = modules.mkAliasAndWrapDefsWithPriority id (options.processChildrenOnly or { });
137 }));
138 default = { };
139 description = lib.mdDoc "Datasets to snapshot.";
140 };
141
142 templates = mkOption {
143 type = types.attrsOf (types.submodule {
144 freeformType = datasetSettingsType;
145 options = commonOptions;
146 });
147 default = { };
148 description = lib.mdDoc "Templates for datasets.";
149 };
150
151 settings = mkOption {
152 type = types.attrsOf datasetSettingsType;
153 description = lib.mdDoc ''
154 Free-form settings written directly to the config file. See
155 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf>
156 for allowed values.
157 '';
158 };
159
160 extraArgs = mkOption {
161 type = types.listOf types.str;
162 default = [ ];
163 example = [ "--verbose" "--readonly" "--debug" ];
164 description = lib.mdDoc ''
165 Extra arguments to pass to sanoid. See
166 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options>
167 for allowed options.
168 '';
169 };
170 };
171
172 # Implementation
173
174 config = mkIf cfg.enable {
175 services.sanoid.settings = mkMerge [
176 (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates)
177 (mapAttrs (d: v: v) cfg.datasets)
178 ];
179
180 systemd.services.sanoid = {
181 description = "Sanoid snapshot service";
182 serviceConfig = {
183 ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
184 ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
185 ExecStart = lib.escapeShellArgs ([
186 "${cfg.package}/bin/sanoid"
187 "--cron"
188 "--configdir"
189 (pkgs.writeTextDir "sanoid.conf" configFile)
190 ] ++ cfg.extraArgs);
191 User = "sanoid";
192 Group = "sanoid";
193 DynamicUser = true;
194 RuntimeDirectory = "sanoid";
195 CacheDirectory = "sanoid";
196 };
197 # Prevents missing snapshots during DST changes
198 environment.TZ = "UTC";
199 after = [ "zfs.target" ];
200 startAt = cfg.interval;
201 };
202 };
203
204 meta.maintainers = with maintainers; [ lopsided98 ];
205}