python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / backup / duplicity.nix
blob05ec997ab66b0175bd9509dcd1199ce642ced37f
1 { config, lib, pkgs, ... }:
3 with lib;
4 let
5   cfg = config.services.duplicity;
7   stateDirectory = "/var/lib/duplicity";
9   localTarget =
10     if hasPrefix "file://" cfg.targetUrl
11     then removePrefix "file://" cfg.targetUrl else null;
15   options.services.duplicity = {
16     enable = mkEnableOption (lib.mdDoc "backups with duplicity");
18     root = mkOption {
19       type = types.path;
20       default = "/";
21       description = lib.mdDoc ''
22         Root directory to backup.
23       '';
24     };
26     include = mkOption {
27       type = types.listOf types.str;
28       default = [ ];
29       example = [ "/home" ];
30       description = lib.mdDoc ''
31         List of paths to include into the backups. See the FILE SELECTION
32         section in {manpage}`duplicity(1)` for details on the syntax.
33       '';
34     };
36     exclude = mkOption {
37       type = types.listOf types.str;
38       default = [ ];
39       description = lib.mdDoc ''
40         List of paths to exclude from backups. See the FILE SELECTION section in
41         {manpage}`duplicity(1)` for details on the syntax.
42       '';
43     };
45     targetUrl = mkOption {
46       type = types.str;
47       example = "s3://host:port/prefix";
48       description = lib.mdDoc ''
49         Target url to backup to. See the URL FORMAT section in
50         {manpage}`duplicity(1)` for supported urls.
51       '';
52     };
54     secretFile = mkOption {
55       type = types.nullOr types.path;
56       default = null;
57       description = lib.mdDoc ''
58         Path of a file containing secrets (gpg passphrase, access key...) in
59         the format of EnvironmentFile as described by
60         {manpage}`systemd.exec(5)`. For example:
61         ```
62         PASSPHRASE=«...»
63         AWS_ACCESS_KEY_ID=«...»
64         AWS_SECRET_ACCESS_KEY=«...»
65         ```
66       '';
67     };
69     frequency = mkOption {
70       type = types.nullOr types.str;
71       default = "daily";
72       description = lib.mdDoc ''
73         Run duplicity with the given frequency (see
74         {manpage}`systemd.time(7)` for the format).
75         If null, do not run automatically.
76       '';
77     };
79     extraFlags = mkOption {
80       type = types.listOf types.str;
81       default = [ ];
82       example = [ "--backend-retry-delay" "100" ];
83       description = lib.mdDoc ''
84         Extra command-line flags passed to duplicity. See
85         {manpage}`duplicity(1)`.
86       '';
87     };
89     fullIfOlderThan = mkOption {
90       type = types.str;
91       default = "never";
92       example = "1M";
93       description = lib.mdDoc ''
94         If `"never"` (the default) always do incremental
95         backups (the first backup will be a full backup, of course).  If
96         `"always"` always do full backups.  Otherwise, this
97         must be a string representing a duration. Full backups will be made
98         when the latest full backup is older than this duration. If this is not
99         the case, an incremental backup is performed.
100       '';
101     };
103     cleanup = {
104       maxAge = mkOption {
105         type = types.nullOr types.str;
106         default = null;
107         example = "6M";
108         description = lib.mdDoc ''
109           If non-null, delete all backup sets older than the given time.  Old backup sets
110           will not be deleted if backup sets newer than time depend on them.
111         '';
112       };
113       maxFull = mkOption {
114         type = types.nullOr types.int;
115         default = null;
116         example = 2;
117         description = lib.mdDoc ''
118           If non-null, delete all backups sets that are older than the count:th last full
119           backup (in other words, keep the last count full backups and
120           associated incremental sets).
121         '';
122       };
123       maxIncr = mkOption {
124         type = types.nullOr types.int;
125         default = null;
126         example = 1;
127         description = lib.mdDoc ''
128           If non-null, delete incremental sets of all backups sets that are
129           older than the count:th last full backup (in other words, keep only
130           old full backups and not their increments).
131         '';
132       };
133     };
134   };
136   config = mkIf cfg.enable {
137     systemd = {
138       services.duplicity = {
139         description = "backup files with duplicity";
141         environment.HOME = stateDirectory;
143         script =
144           let
145             target = escapeShellArg cfg.targetUrl;
146             extra = escapeShellArgs ([ "--archive-dir" stateDirectory ] ++ cfg.extraFlags);
147             dup = "${pkgs.duplicity}/bin/duplicity";
148           in
149           ''
150             set -x
151             ${dup} cleanup ${target} --force ${extra}
152             ${lib.optionalString (cfg.cleanup.maxAge != null) "${dup} remove-older-than ${lib.escapeShellArg cfg.cleanup.maxAge} ${target} --force ${extra}"}
153             ${lib.optionalString (cfg.cleanup.maxFull != null) "${dup} remove-all-but-n-full ${toString cfg.cleanup.maxFull} ${target} --force ${extra}"}
154             ${lib.optionalString (cfg.cleanup.maxIncr != null) "${dup} remove-all-inc-of-but-n-full ${toString cfg.cleanup.maxIncr} ${target} --force ${extra}"}
155             exec ${dup} ${if cfg.fullIfOlderThan == "always" then "full" else "incr"} ${lib.escapeShellArgs (
156               [ cfg.root cfg.targetUrl ]
157               ++ concatMap (p: [ "--include" p ]) cfg.include
158               ++ concatMap (p: [ "--exclude" p ]) cfg.exclude
159               ++ (lib.optionals (cfg.fullIfOlderThan != "never" && cfg.fullIfOlderThan != "always") [ "--full-if-older-than" cfg.fullIfOlderThan ])
160               )} ${extra}
161           '';
162         serviceConfig = {
163           PrivateTmp = true;
164           ProtectSystem = "strict";
165           ProtectHome = "read-only";
166           StateDirectory = baseNameOf stateDirectory;
167         } // optionalAttrs (localTarget != null) {
168           ReadWritePaths = localTarget;
169         } // optionalAttrs (cfg.secretFile != null) {
170           EnvironmentFile = cfg.secretFile;
171         };
172       } // optionalAttrs (cfg.frequency != null) {
173         startAt = cfg.frequency;
174       };
176       tmpfiles.rules = optional (localTarget != null) "d ${localTarget} 0700 root root -";
177     };
179     assertions = singleton {
180       # Duplicity will fail if the last file selection option is an include. It
181       # is not always possible to detect but this simple case can be caught.
182       assertion = cfg.include != [ ] -> cfg.exclude != [ ] || cfg.extraFlags != [ ];
183       message = ''
184         Duplicity will fail if you only specify included paths ("Because the
185         default is to include all files, the expression is redundant. Exiting
186         because this probably isn't what you meant.")
187       '';
188     };
189   };