python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / backup / restic.nix
blob869ed5d9976c35c59c8714b491217ce1fd50110b
1 { config, lib, pkgs, utils, ... }:
3 with lib;
5 let
6   # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers"
7   inherit (utils.systemdUtils.unitOptions) unitOption;
8 in
10   options.services.restic.backups = mkOption {
11     description = lib.mdDoc ''
12       Periodic backups to create with Restic.
13     '';
14     type = types.attrsOf (types.submodule ({ config, name, ... }: {
15       options = {
16         passwordFile = mkOption {
17           type = types.str;
18           description = lib.mdDoc ''
19             Read the repository password from a file.
20           '';
21           example = "/etc/nixos/restic-password";
22         };
24         environmentFile = mkOption {
25           type = with types; nullOr str;
26           # added on 2021-08-28, s3CredentialsFile should
27           # be removed in the future (+ remember the warning)
28           default = config.s3CredentialsFile;
29           description = lib.mdDoc ''
30             file containing the credentials to access the repository, in the
31             format of an EnvironmentFile as described by systemd.exec(5)
32           '';
33         };
35         s3CredentialsFile = mkOption {
36           type = with types; nullOr str;
37           default = null;
38           description = lib.mdDoc ''
39             file containing the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
40             for an S3-hosted repository, in the format of an EnvironmentFile
41             as described by systemd.exec(5)
42           '';
43         };
45         rcloneOptions = mkOption {
46           type = with types; nullOr (attrsOf (oneOf [ str bool ]));
47           default = null;
48           description = lib.mdDoc ''
49             Options to pass to rclone to control its behavior.
50             See <https://rclone.org/docs/#options> for
51             available options. When specifying option names, strip the
52             leading `--`. To set a flag such as
53             `--drive-use-trash`, which does not take a value,
54             set the value to the Boolean `true`.
55           '';
56           example = {
57             bwlimit = "10M";
58             drive-use-trash = "true";
59           };
60         };
62         rcloneConfig = mkOption {
63           type = with types; nullOr (attrsOf (oneOf [ str bool ]));
64           default = null;
65           description = lib.mdDoc ''
66             Configuration for the rclone remote being used for backup.
67             See the remote's specific options under rclone's docs at
68             <https://rclone.org/docs/>. When specifying
69             option names, use the "config" name specified in the docs.
70             For example, to set `--b2-hard-delete` for a B2
71             remote, use `hard_delete = true` in the
72             attribute set.
73             Warning: Secrets set in here will be world-readable in the Nix
74             store! Consider using the `rcloneConfigFile`
75             option instead to specify secret values separately. Note that
76             options set here will override those set in the config file.
77           '';
78           example = {
79             type = "b2";
80             account = "xxx";
81             key = "xxx";
82             hard_delete = true;
83           };
84         };
86         rcloneConfigFile = mkOption {
87           type = with types; nullOr path;
88           default = null;
89           description = lib.mdDoc ''
90             Path to the file containing rclone configuration. This file
91             must contain configuration for the remote specified in this backup
92             set and also must be readable by root. Options set in
93             `rcloneConfig` will override those set in this
94             file.
95           '';
96         };
98         repository = mkOption {
99           type = with types; nullOr str;
100           default = null;
101           description = lib.mdDoc ''
102             repository to backup to.
103           '';
104           example = "sftp:backup@192.168.1.100:/backups/${name}";
105         };
107         repositoryFile = mkOption {
108           type = with types; nullOr path;
109           default = null;
110           description = lib.mdDoc ''
111             Path to the file containing the repository location to backup to.
112           '';
113         };
115         paths = mkOption {
116           type = types.nullOr (types.listOf types.str);
117           default = null;
118           description = lib.mdDoc ''
119             Which paths to backup.  If null or an empty array, no
120             backup command will be run.  This can be used to create a
121             prune-only job.
122           '';
123           example = [
124             "/var/lib/postgresql"
125             "/home/user/backup"
126           ];
127         };
129         timerConfig = mkOption {
130           type = types.attrsOf unitOption;
131           default = {
132             OnCalendar = "daily";
133           };
134           description = lib.mdDoc ''
135             When to run the backup. See man systemd.timer for details.
136           '';
137           example = {
138             OnCalendar = "00:05";
139             RandomizedDelaySec = "5h";
140           };
141         };
143         user = mkOption {
144           type = types.str;
145           default = "root";
146           description = lib.mdDoc ''
147             As which user the backup should run.
148           '';
149           example = "postgresql";
150         };
152         extraBackupArgs = mkOption {
153           type = types.listOf types.str;
154           default = [ ];
155           description = lib.mdDoc ''
156             Extra arguments passed to restic backup.
157           '';
158           example = [
159             "--exclude-file=/etc/nixos/restic-ignore"
160           ];
161         };
163         extraOptions = mkOption {
164           type = types.listOf types.str;
165           default = [ ];
166           description = lib.mdDoc ''
167             Extra extended options to be passed to the restic --option flag.
168           '';
169           example = [
170             "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'"
171           ];
172         };
174         initialize = mkOption {
175           type = types.bool;
176           default = false;
177           description = lib.mdDoc ''
178             Create the repository if it doesn't exist.
179           '';
180         };
182         pruneOpts = mkOption {
183           type = types.listOf types.str;
184           default = [ ];
185           description = lib.mdDoc ''
186             A list of options (--keep-\* et al.) for 'restic forget
187             --prune', to automatically prune old snapshots.  The
188             'forget' command is run *after* the 'backup' command, so
189             keep that in mind when constructing the --keep-\* options.
190           '';
191           example = [
192             "--keep-daily 7"
193             "--keep-weekly 5"
194             "--keep-monthly 12"
195             "--keep-yearly 75"
196           ];
197         };
199         checkOpts = mkOption {
200           type = types.listOf types.str;
201           default = [ ];
202           description = lib.mdDoc ''
203             A list of options for 'restic check', which is run after
204             pruning.
205           '';
206           example = [
207             "--with-cache"
208           ];
209         };
211         dynamicFilesFrom = mkOption {
212           type = with types; nullOr str;
213           default = null;
214           description = lib.mdDoc ''
215             A script that produces a list of files to back up.  The
216             results of this command are given to the '--files-from'
217             option.
218           '';
219           example = "find /home/matt/git -type d -name .git";
220         };
222         backupPrepareCommand = mkOption {
223           type = with types; nullOr str;
224           default = null;
225           description = lib.mdDoc ''
226             A script that must run before starting the backup process.
227           '';
228         };
230         backupCleanupCommand = mkOption {
231           type = with types; nullOr str;
232           default = null;
233           description = lib.mdDoc ''
234             A script that must run after finishing the backup process.
235           '';
236         };
238         package = mkOption {
239           type = types.package;
240           default = pkgs.restic;
241           defaultText = literalExpression "pkgs.restic";
242           description = lib.mdDoc ''
243             Restic package to use.
244           '';
245         };
246       };
247     }));
248     default = { };
249     example = {
250       localbackup = {
251         paths = [ "/home" ];
252         repository = "/mnt/backup-hdd";
253         passwordFile = "/etc/nixos/secrets/restic-password";
254         initialize = true;
255       };
256       remotebackup = {
257         paths = [ "/home" ];
258         repository = "sftp:backup@host:/backups/home";
259         passwordFile = "/etc/nixos/secrets/restic-password";
260         extraOptions = [
261           "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'"
262         ];
263         timerConfig = {
264           OnCalendar = "00:05";
265           RandomizedDelaySec = "5h";
266         };
267       };
268     };
269   };
271   config = {
272     warnings = mapAttrsToList (n: v: "services.restic.backups.${n}.s3CredentialsFile is deprecated, please use services.restic.backups.${n}.environmentFile instead.") (filterAttrs (n: v: v.s3CredentialsFile != null) config.services.restic.backups);
273     systemd.services =
274       mapAttrs'
275         (name: backup:
276           let
277             extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
278             resticCmd = "${backup.package}/bin/restic${extraOptions}";
279             filesFromTmpFile = "/run/restic-backups-${name}/includes";
280             backupPaths =
281               if (backup.dynamicFilesFrom == null)
282               then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
283               else "--files-from ${filesFromTmpFile}";
284             pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
285               (resticCmd + " forget --prune --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.pruneOpts))
286               (resticCmd + " check --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.checkOpts))
287             ];
288             # Helper functions for rclone remotes
289             rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
290             rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v);
291             rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v);
292             toRcloneVal = v: if lib.isBool v then lib.boolToString v else v;
293           in
294           nameValuePair "restic-backups-${name}" ({
295             environment = {
296               RESTIC_PASSWORD_FILE = backup.passwordFile;
297               RESTIC_REPOSITORY = backup.repository;
298               RESTIC_REPOSITORY_FILE = backup.repositoryFile;
299             } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs'
300               (name: value:
301                 nameValuePair (rcloneAttrToOpt name) (toRcloneVal value)
302               )
303               backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) {
304               RCLONE_CONFIG = backup.rcloneConfigFile;
305             } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs'
306               (name: value:
307                 nameValuePair (rcloneAttrToConf name) (toRcloneVal value)
308               )
309               backup.rcloneConfig);
310             path = [ pkgs.openssh ];
311             restartIfChanged = false;
312             serviceConfig = {
313               Type = "oneshot";
314               ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
315                 ++ pruneCmd;
316               User = backup.user;
317               RuntimeDirectory = "restic-backups-${name}";
318               CacheDirectory = "restic-backups-${name}";
319               CacheDirectoryMode = "0700";
320             } // optionalAttrs (backup.environmentFile != null) {
321               EnvironmentFile = backup.environmentFile;
322             };
323           } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) {
324             preStart = ''
325               ${optionalString (backup.backupPrepareCommand != null) ''
326                 ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand}
327               ''}
328               ${optionalString (backup.initialize) ''
329                 ${resticCmd} snapshots || ${resticCmd} init
330               ''}
331               ${optionalString (backup.dynamicFilesFrom != null) ''
332                 ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile}
333               ''}
334             '';
335           } // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) {
336             postStop = ''
337               ${optionalString (backup.backupCleanupCommand != null) ''
338                 ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand}
339               ''}
340               ${optionalString (backup.dynamicFilesFrom != null) ''
341                 rm ${filesFromTmpFile}
342               ''}
343             '';
344           })
345         )
346         config.services.restic.backups;
347     systemd.timers =
348       mapAttrs'
349         (name: backup: nameValuePair "restic-backups-${name}" {
350           wantedBy = [ "timers.target" ];
351           timerConfig = backup.timerConfig;
352         })
353         config.services.restic.backups;
354   };