at 22.05-pre 11 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" 7 unitOption = (import ../../system/boot/systemd-unit-options.nix { inherit config lib; }).unitOption; 8in 9{ 10 options.services.restic.backups = mkOption { 11 description = '' 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 = '' 19 Read the repository password from a file. 20 ''; 21 example = "/etc/nixos/restic-password"; 22 }; 23 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 = '' 30 file containing the credentials to access the repository, in the 31 format of an EnvironmentFile as described by systemd.exec(5) 32 ''; 33 }; 34 35 s3CredentialsFile = mkOption { 36 type = with types; nullOr str; 37 default = null; 38 description = '' 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 }; 44 45 rcloneOptions = mkOption { 46 type = with types; nullOr (attrsOf (oneOf [ str bool ])); 47 default = null; 48 description = '' 49 Options to pass to rclone to control its behavior. 50 See <link xlink:href="https://rclone.org/docs/#options"/> for 51 available options. When specifying option names, strip the 52 leading <literal>--</literal>. To set a flag such as 53 <literal>--drive-use-trash</literal>, which does not take a value, 54 set the value to the Boolean <literal>true</literal>. 55 ''; 56 example = { 57 bwlimit = "10M"; 58 drive-use-trash = "true"; 59 }; 60 }; 61 62 rcloneConfig = mkOption { 63 type = with types; nullOr (attrsOf (oneOf [ str bool ])); 64 default = null; 65 description = '' 66 Configuration for the rclone remote being used for backup. 67 See the remote's specific options under rclone's docs at 68 <link xlink:href="https://rclone.org/docs/"/>. When specifying 69 option names, use the "config" name specified in the docs. 70 For example, to set <literal>--b2-hard-delete</literal> for a B2 71 remote, use <literal>hard_delete = true</literal> in the 72 attribute set. 73 Warning: Secrets set in here will be world-readable in the Nix 74 store! Consider using the <literal>rcloneConfigFile</literal> 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 }; 85 86 rcloneConfigFile = mkOption { 87 type = with types; nullOr path; 88 default = null; 89 description = '' 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 <literal>rcloneConfig</literal> will override those set in this 94 file. 95 ''; 96 }; 97 98 repository = mkOption { 99 type = types.str; 100 description = '' 101 repository to backup to. 102 ''; 103 example = "sftp:backup@192.168.1.100:/backups/${name}"; 104 }; 105 106 paths = mkOption { 107 type = types.nullOr (types.listOf types.str); 108 default = null; 109 description = '' 110 Which paths to backup. If null or an empty array, no 111 backup command will be run. This can be used to create a 112 prune-only job. 113 ''; 114 example = [ 115 "/var/lib/postgresql" 116 "/home/user/backup" 117 ]; 118 }; 119 120 timerConfig = mkOption { 121 type = types.attrsOf unitOption; 122 default = { 123 OnCalendar = "daily"; 124 }; 125 description = '' 126 When to run the backup. See man systemd.timer for details. 127 ''; 128 example = { 129 OnCalendar = "00:05"; 130 RandomizedDelaySec = "5h"; 131 }; 132 }; 133 134 user = mkOption { 135 type = types.str; 136 default = "root"; 137 description = '' 138 As which user the backup should run. 139 ''; 140 example = "postgresql"; 141 }; 142 143 extraBackupArgs = mkOption { 144 type = types.listOf types.str; 145 default = []; 146 description = '' 147 Extra arguments passed to restic backup. 148 ''; 149 example = [ 150 "--exclude-file=/etc/nixos/restic-ignore" 151 ]; 152 }; 153 154 extraOptions = mkOption { 155 type = types.listOf types.str; 156 default = []; 157 description = '' 158 Extra extended options to be passed to the restic --option flag. 159 ''; 160 example = [ 161 "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" 162 ]; 163 }; 164 165 initialize = mkOption { 166 type = types.bool; 167 default = false; 168 description = '' 169 Create the repository if it doesn't exist. 170 ''; 171 }; 172 173 pruneOpts = mkOption { 174 type = types.listOf types.str; 175 default = []; 176 description = '' 177 A list of options (--keep-* et al.) for 'restic forget 178 --prune', to automatically prune old snapshots. The 179 'forget' command is run *after* the 'backup' command, so 180 keep that in mind when constructing the --keep-* options. 181 ''; 182 example = [ 183 "--keep-daily 7" 184 "--keep-weekly 5" 185 "--keep-monthly 12" 186 "--keep-yearly 75" 187 ]; 188 }; 189 190 dynamicFilesFrom = mkOption { 191 type = with types; nullOr str; 192 default = null; 193 description = '' 194 A script that produces a list of files to back up. The 195 results of this command are given to the '--files-from' 196 option. 197 ''; 198 example = "find /home/matt/git -type d -name .git"; 199 }; 200 }; 201 })); 202 default = {}; 203 example = { 204 localbackup = { 205 paths = [ "/home" ]; 206 repository = "/mnt/backup-hdd"; 207 passwordFile = "/etc/nixos/secrets/restic-password"; 208 initialize = true; 209 }; 210 remotebackup = { 211 paths = [ "/home" ]; 212 repository = "sftp:backup@host:/backups/home"; 213 passwordFile = "/etc/nixos/secrets/restic-password"; 214 extraOptions = [ 215 "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'" 216 ]; 217 timerConfig = { 218 OnCalendar = "00:05"; 219 RandomizedDelaySec = "5h"; 220 }; 221 }; 222 }; 223 }; 224 225 config = { 226 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); 227 systemd.services = 228 mapAttrs' (name: backup: 229 let 230 extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; 231 resticCmd = "${pkgs.restic}/bin/restic${extraOptions}"; 232 filesFromTmpFile = "/run/restic-backups-${name}/includes"; 233 backupPaths = if (backup.dynamicFilesFrom == null) 234 then if (backup.paths != null) then concatStringsSep " " backup.paths else "" 235 else "--files-from ${filesFromTmpFile}"; 236 pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ 237 ( resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts) ) 238 ( resticCmd + " check" ) 239 ]; 240 # Helper functions for rclone remotes 241 rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1; 242 rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); 243 rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); 244 toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; 245 in nameValuePair "restic-backups-${name}" ({ 246 environment = { 247 RESTIC_PASSWORD_FILE = backup.passwordFile; 248 RESTIC_REPOSITORY = backup.repository; 249 } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' (name: value: 250 nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) 251 ) backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { 252 RCLONE_CONFIG = backup.rcloneConfigFile; 253 } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' (name: value: 254 nameValuePair (rcloneAttrToConf name) (toRcloneVal value) 255 ) backup.rcloneConfig); 256 path = [ pkgs.openssh ]; 257 restartIfChanged = false; 258 serviceConfig = { 259 Type = "oneshot"; 260 ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ]) 261 ++ pruneCmd; 262 User = backup.user; 263 RuntimeDirectory = "restic-backups-${name}"; 264 CacheDirectory = "restic-backups-${name}"; 265 CacheDirectoryMode = "0700"; 266 } // optionalAttrs (backup.environmentFile != null) { 267 EnvironmentFile = backup.environmentFile; 268 }; 269 } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null) { 270 preStart = '' 271 ${optionalString (backup.initialize) '' 272 ${resticCmd} snapshots || ${resticCmd} init 273 ''} 274 ${optionalString (backup.dynamicFilesFrom != null) '' 275 ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile} 276 ''} 277 ''; 278 } // optionalAttrs (backup.dynamicFilesFrom != null) { 279 postStart = '' 280 rm ${filesFromTmpFile} 281 ''; 282 }) 283 ) config.services.restic.backups; 284 systemd.timers = 285 mapAttrs' (name: backup: nameValuePair "restic-backups-${name}" { 286 wantedBy = [ "timers.target" ]; 287 timerConfig = backup.timerConfig; 288 }) config.services.restic.backups; 289 }; 290}