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