1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.sanoid;
9
10 datasetSettingsType =
11 with lib.types;
12 (attrsOf (
13 nullOr (oneOf [
14 str
15 int
16 bool
17 (listOf str)
18 ])
19 ))
20 // {
21 description = "dataset/template options";
22 };
23
24 commonOptions = {
25 hourly = lib.mkOption {
26 description = "Number of hourly snapshots.";
27 type = with lib.types; nullOr ints.unsigned;
28 default = null;
29 };
30
31 daily = lib.mkOption {
32 description = "Number of daily snapshots.";
33 type = with lib.types; nullOr ints.unsigned;
34 default = null;
35 };
36
37 monthly = lib.mkOption {
38 description = "Number of monthly snapshots.";
39 type = with lib.types; nullOr ints.unsigned;
40 default = null;
41 };
42
43 yearly = lib.mkOption {
44 description = "Number of yearly snapshots.";
45 type = with lib.types; nullOr ints.unsigned;
46 default = null;
47 };
48
49 autoprune = lib.mkOption {
50 description = "Whether to automatically prune old snapshots.";
51 type = with lib.types; nullOr bool;
52 default = null;
53 };
54
55 autosnap = lib.mkOption {
56 description = "Whether to automatically take snapshots.";
57 type = with lib.types; nullOr bool;
58 default = null;
59 };
60 };
61
62 datasetOptions = rec {
63 use_template = lib.mkOption {
64 description = "Names of the templates to use for this dataset.";
65 type = lib.types.listOf (
66 lib.types.str
67 // {
68 check = (lib.types.enum (lib.attrNames cfg.templates)).check;
69 description = "configured template name";
70 }
71 );
72 default = [ ];
73 };
74 useTemplate = use_template;
75
76 recursive = lib.mkOption {
77 description = ''
78 Whether to recursively snapshot dataset children.
79 You can also set this to `"zfs"` to handle datasets
80 recursively in an atomic way without the possibility to
81 override settings for child datasets.
82 '';
83 type =
84 with lib.types;
85 oneOf [
86 bool
87 (enum [ "zfs" ])
88 ];
89 default = false;
90 };
91
92 process_children_only = lib.mkOption {
93 description = "Whether to only snapshot child datasets if recursing.";
94 type = lib.types.bool;
95 default = false;
96 };
97 processChildrenOnly = process_children_only;
98 };
99
100 # Extract unique dataset names
101 datasets = lib.unique (lib.attrNames cfg.datasets);
102
103 # Function to build "zfs allow" and "zfs unallow" commands for the
104 # filesystems we've delegated permissions to.
105 buildAllowCommand =
106 zfsAction: permissions: dataset:
107 lib.escapeShellArgs [
108 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS
109 "-+/run/booted-system/sw/bin/zfs"
110 zfsAction
111 "sanoid"
112 (lib.concatStringsSep "," permissions)
113 dataset
114 ];
115
116 configFile =
117 let
118 mkValueString =
119 v: if lib.isList v then lib.concatStringsSep "," v else lib.generators.mkValueStringDefault { } v;
120
121 mkKeyValue =
122 k: v:
123 if v == null then
124 ""
125 else if k == "processChildrenOnly" then
126 ""
127 else if k == "useTemplate" then
128 ""
129 else
130 lib.generators.mkKeyValueDefault { inherit mkValueString; } "=" k v;
131 in
132 lib.generators.toINI { inherit mkKeyValue; } cfg.settings;
133
134in
135{
136
137 # Interface
138
139 options.services.sanoid = {
140 enable = lib.mkEnableOption "Sanoid ZFS snapshotting service";
141
142 package = lib.mkPackageOption pkgs "sanoid" { };
143
144 interval = lib.mkOption {
145 type = lib.types.str;
146 default = "hourly";
147 example = "daily";
148 description = ''
149 Run sanoid at this interval. The default is to run hourly.
150
151 The format is described in
152 {manpage}`systemd.time(7)`.
153 '';
154 };
155
156 datasets = lib.mkOption {
157 type = lib.types.attrsOf (
158 lib.types.submodule (
159 { config, options, ... }:
160 {
161 freeformType = datasetSettingsType;
162 options = commonOptions // datasetOptions;
163 config.use_template = lib.modules.mkAliasAndWrapDefsWithPriority lib.id (
164 options.useTemplate or { }
165 );
166 config.process_children_only = lib.modules.mkAliasAndWrapDefsWithPriority lib.id (
167 options.processChildrenOnly or { }
168 );
169 }
170 )
171 );
172 default = { };
173 description = "Datasets to snapshot.";
174 };
175
176 templates = lib.mkOption {
177 type = lib.types.attrsOf (
178 lib.types.submodule {
179 freeformType = datasetSettingsType;
180 options = commonOptions;
181 }
182 );
183 default = { };
184 description = "Templates for datasets.";
185 };
186
187 settings = lib.mkOption {
188 type = lib.types.attrsOf datasetSettingsType;
189 description = ''
190 Free-form settings written directly to the config file. See
191 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf>
192 for allowed values.
193 '';
194 };
195
196 extraArgs = lib.mkOption {
197 type = lib.types.listOf lib.types.str;
198 default = [ ];
199 example = [
200 "--verbose"
201 "--readonly"
202 "--debug"
203 ];
204 description = ''
205 Extra arguments to pass to sanoid. See
206 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options>
207 for allowed options.
208 '';
209 };
210 };
211
212 # Implementation
213
214 config = lib.mkIf cfg.enable {
215 services.sanoid.settings = lib.mkMerge [
216 (lib.mapAttrs' (d: v: lib.nameValuePair ("template_" + d) v) cfg.templates)
217 (lib.mapAttrs (d: v: v) cfg.datasets)
218 ];
219
220 systemd.services.sanoid = {
221 description = "Sanoid snapshot service";
222 serviceConfig = {
223 ExecStartPre = (
224 map (buildAllowCommand "allow" [
225 "snapshot"
226 "mount"
227 "destroy"
228 ]) datasets
229 );
230 ExecStopPost = (
231 map (buildAllowCommand "unallow" [
232 "snapshot"
233 "mount"
234 "destroy"
235 ]) datasets
236 );
237 ExecStart = lib.escapeShellArgs (
238 [
239 "${cfg.package}/bin/sanoid"
240 "--cron"
241 "--configdir"
242 (pkgs.writeTextDir "sanoid.conf" configFile)
243 ]
244 ++ cfg.extraArgs
245 );
246 User = "sanoid";
247 Group = "sanoid";
248 DynamicUser = true;
249 RuntimeDirectory = "sanoid";
250 CacheDirectory = "sanoid";
251 };
252 # Prevents missing snapshots during DST changes
253 environment.TZ = "UTC";
254 after = [ "zfs.target" ];
255 startAt = cfg.interval;
256 };
257 };
258
259 meta.maintainers = with lib.maintainers; [ lopsided98 ];
260}