at 23.05-pre 13 kB view raw
1{ config, lib, pkgs, utils, ... }: 2 3with lib; 4 5let 6 # Type for a valid systemd unit option. Needed for correctly passing "timerConfig" to "systemd.timers" 7 inherit (utils.systemdUtils.unitOptions) unitOption; 8in 9{ 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 }; 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 = 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 }; 34 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 }; 44 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 }; 61 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 }; 85 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 }; 97 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 }; 106 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 }; 114 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 }; 128 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 }; 142 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 }; 151 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 }; 162 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 }; 173 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 }; 181 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 }; 198 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 }; 210 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 }; 221 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 }; 229 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 }; 237 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 }; 270 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 }; 355}