grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / snapper.nix
blob42f782bcd6e188674bc5358d69948935bce7dc7d
2   config,
3   pkgs,
4   lib,
5   ...
6 }:
7 let
8   cfg = config.services.snapper;
10   mkValue =
11     v:
12     if lib.isList v then
13       "\"${
14         lib.concatMapStringsSep " " (lib.escape [
15           "\\"
16           " "
17         ]) v
18       }\""
19     else if v == true then
20       "yes"
21     else if v == false then
22       "no"
23     else if lib.isString v then
24       "\"${v}\""
25     else
26       builtins.toJSON v;
28   mkKeyValue = k: v: "${k}=${mkValue v}";
30   # "it's recommended to always specify the filesystem type"  -- man snapper-configs
31   defaultOf = k: if k == "FSTYPE" then null else configOptions.${k}.default or null;
33   safeStr = lib.types.strMatching "[^\n\"]*" // {
34     description = "string without line breaks or quotes";
35     descriptionClass = "conjunction";
36   };
38   intOrNumberOrRange = lib.types.either lib.types.ints.unsigned (
39     lib.types.strMatching "[[:digit:]]+(\-[[:digit:]]+)?"
40     // {
41       description = "string containing either a number or a range";
42       descriptionClass = "conjunction";
43     }
44   );
46   configOptions = {
47     SUBVOLUME = lib.mkOption {
48       type = lib.types.path;
49       description = ''
50         Path of the subvolume or mount point.
51         This path is a subvolume and has to contain a subvolume named
52         .snapshots.
53         See also man:snapper(8) section PERMISSIONS.
54       '';
55     };
57     FSTYPE = lib.mkOption {
58       type = lib.types.enum [ "btrfs" ];
59       default = "btrfs";
60       description = ''
61         Filesystem type. Only btrfs is stable and tested.
62       '';
63     };
65     ALLOW_GROUPS = lib.mkOption {
66       type = lib.types.listOf safeStr;
67       default = [ ];
68       description = ''
69         List of groups allowed to operate with the config.
71         Also see the PERMISSIONS section in man:snapper(8).
72       '';
73     };
75     ALLOW_USERS = lib.mkOption {
76       type = lib.types.listOf safeStr;
77       default = [ ];
78       example = [ "alice" ];
79       description = ''
80         List of users allowed to operate with the config. "root" is always
81         implicitly included.
83         Also see the PERMISSIONS section in man:snapper(8).
84       '';
85     };
87     TIMELINE_CLEANUP = lib.mkOption {
88       type = lib.types.bool;
89       default = false;
90       description = ''
91         Defines whether the timeline cleanup algorithm should be run for the config.
92       '';
93     };
95     TIMELINE_CREATE = lib.mkOption {
96       type = lib.types.bool;
97       default = false;
98       description = ''
99         Defines whether hourly snapshots should be created.
100       '';
101     };
103     TIMELINE_LIMIT_HOURLY = lib.mkOption {
104       type = intOrNumberOrRange;
105       default = 10;
106       description = ''
107         Limits for timeline cleanup.
108       '';
109     };
111     TIMELINE_LIMIT_DAILY = lib.mkOption {
112       type = intOrNumberOrRange;
113       default = 10;
114       description = ''
115         Limits for timeline cleanup.
116       '';
117     };
119     TIMELINE_LIMIT_WEEKLY = lib.mkOption {
120       type = intOrNumberOrRange;
121       default = 0;
122       description = ''
123         Limits for timeline cleanup.
124       '';
125     };
127     TIMELINE_LIMIT_MONTHLY = lib.mkOption {
128       type = intOrNumberOrRange;
129       default = 10;
130       description = ''
131         Limits for timeline cleanup.
132       '';
133     };
135     TIMELINE_LIMIT_QUARTERLY = lib.mkOption {
136       type = intOrNumberOrRange;
137       default = 0;
138       description = ''
139         Limits for timeline cleanup.
140       '';
141     };
143     TIMELINE_LIMIT_YEARLY = lib.mkOption {
144       type = intOrNumberOrRange;
145       default = 10;
146       description = ''
147         Limits for timeline cleanup.
148       '';
149     };
150   };
154   options.services.snapper = {
156     snapshotRootOnBoot = lib.mkOption {
157       type = lib.types.bool;
158       default = false;
159       description = ''
160         Whether to snapshot root on boot
161       '';
162     };
164     snapshotInterval = lib.mkOption {
165       type = lib.types.str;
166       default = "hourly";
167       description = ''
168         Snapshot interval.
170         The format is described in
171         {manpage}`systemd.time(7)`.
172       '';
173     };
175     persistentTimer = lib.mkOption {
176       default = false;
177       type = lib.types.bool;
178       example = true;
179       description = ''
180         Set the `Persistent` option for the
181         {manpage}`systemd.timer(5)`
182         which triggers the snapshot immediately if the last trigger
183         was missed (e.g. if the system was powered down).
184       '';
185     };
187     cleanupInterval = lib.mkOption {
188       type = lib.types.str;
189       default = "1d";
190       description = ''
191         Cleanup interval.
193         The format is described in
194         {manpage}`systemd.time(7)`.
195       '';
196     };
198     filters = lib.mkOption {
199       type = lib.types.nullOr lib.types.lines;
200       default = null;
201       description = ''
202         Global display difference filter. See man:snapper(8) for more details.
203       '';
204     };
206     configs = lib.mkOption {
207       default = { };
208       example = lib.literalExpression ''
209         {
210           home = {
211             SUBVOLUME = "/home";
212             ALLOW_USERS = [ "alice" ];
213             TIMELINE_CREATE = true;
214             TIMELINE_CLEANUP = true;
215           };
216         }
217       '';
219       description = ''
220         Subvolume configuration. Any option mentioned in man:snapper-configs(5)
221         is valid here, even if NixOS doesn't document it.
222       '';
224       type = lib.types.attrsOf (
225         lib.types.submodule {
226           freeformType = lib.types.attrsOf (
227             lib.types.oneOf [
228               (lib.types.listOf safeStr)
229               lib.types.bool
230               safeStr
231               lib.types.number
232             ]
233           );
235           options = configOptions;
236         }
237       );
238     };
239   };
241   config = lib.mkIf (cfg.configs != { }) (
242     let
243       documentation = [
244         "man:snapper(8)"
245         "man:snapper-configs(5)"
246       ];
247     in
248     {
249       environment = {
251         systemPackages = [ pkgs.snapper ];
253         # Note: snapper/config-templates/default is only needed for create-config
254         #       which is not the NixOS way to configure.
255         etc =
256           {
258             "sysconfig/snapper".text = ''
259               SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}"
260             '';
261           }
262           // (lib.mapAttrs' (
263             name: subvolume:
264             lib.nameValuePair "snapper/configs/${name}" ({
265               text = lib.generators.toKeyValue { inherit mkKeyValue; } (
266                 lib.filterAttrs (k: v: v != defaultOf k) subvolume
267               );
268             })
269           ) cfg.configs)
270           // (lib.optionalAttrs (cfg.filters != null) { "snapper/filters/default.txt".text = cfg.filters; });
271       };
273       services.dbus.packages = [ pkgs.snapper ];
275       systemd.services.snapperd = {
276         description = "DBus interface for snapper";
277         inherit documentation;
278         serviceConfig = {
279           Type = "dbus";
280           BusName = "org.opensuse.Snapper";
281           ExecStart = "${pkgs.snapper}/bin/snapperd";
282           CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE";
283           LockPersonality = true;
284           NoNewPrivileges = false;
285           PrivateNetwork = true;
286           ProtectHostname = true;
287           RestrictAddressFamilies = "AF_UNIX";
288           RestrictRealtime = true;
289         };
290       };
292       systemd.services.snapper-timeline = {
293         description = "Timeline of Snapper Snapshots";
294         inherit documentation;
295         requires = [ "local-fs.target" ];
296         serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline";
297       };
299       systemd.timers.snapper-timeline = {
300         wantedBy = [ "timers.target" ];
301         timerConfig = {
302           Persistent = cfg.persistentTimer;
303           OnCalendar = cfg.snapshotInterval;
304         };
305       };
307       systemd.services.snapper-cleanup = {
308         description = "Cleanup of Snapper Snapshots";
309         inherit documentation;
310         serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup";
311       };
313       systemd.timers.snapper-cleanup = {
314         description = "Cleanup of Snapper Snapshots";
315         inherit documentation;
316         wantedBy = [ "timers.target" ];
317         requires = [ "local-fs.target" ];
318         timerConfig.OnBootSec = "10m";
319         timerConfig.OnUnitActiveSec = cfg.cleanupInterval;
320       };
322       systemd.services.snapper-boot = lib.mkIf cfg.snapshotRootOnBoot {
323         description = "Take snapper snapshot of root on boot";
324         inherit documentation;
325         serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
326         serviceConfig.Type = "oneshot";
327         requires = [ "local-fs.target" ];
328         wantedBy = [ "multi-user.target" ];
329         unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
330       };
332       assertions = lib.concatMap (
333         name:
334         let
335           sub = cfg.configs.${name};
336         in
337         [
338           {
339             assertion = !(sub ? extraConfig);
340             message = ''
341               The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it.
342               The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'.
343             '';
344           }
345         ]
346         ++
347           map
348             (attr: {
349               assertion = !(lib.hasAttr attr sub);
350               message = ''
351                 The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${lib.toUpper attr}'.
352               '';
353             })
354             [
355               "fstype"
356               "subvolume"
357             ]
358       ) (lib.attrNames cfg.configs);
359     }
360   );
362   meta.maintainers = with lib.maintainers; [ Djabx ];