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