at 23.11-pre 14 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 exclude = mkOption { 130 type = types.listOf types.str; 131 default = [ ]; 132 description = lib.mdDoc '' 133 Patterns to exclude when backing up. See 134 https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for 135 details on syntax. 136 ''; 137 example = [ 138 "/var/cache" 139 "/home/*/.cache" 140 ".git" 141 ]; 142 }; 143 144 timerConfig = mkOption { 145 type = types.attrsOf unitOption; 146 default = { 147 OnCalendar = "daily"; 148 Persistent = true; 149 }; 150 description = lib.mdDoc '' 151 When to run the backup. See {manpage}`systemd.timer(5)` for details. 152 ''; 153 example = { 154 OnCalendar = "00:05"; 155 RandomizedDelaySec = "5h"; 156 Persistent = true; 157 }; 158 }; 159 160 user = mkOption { 161 type = types.str; 162 default = "root"; 163 description = lib.mdDoc '' 164 As which user the backup should run. 165 ''; 166 example = "postgresql"; 167 }; 168 169 extraBackupArgs = mkOption { 170 type = types.listOf types.str; 171 default = [ ]; 172 description = lib.mdDoc '' 173 Extra arguments passed to restic backup. 174 ''; 175 example = [ 176 "--exclude-file=/etc/nixos/restic-ignore" 177 ]; 178 }; 179 180 extraOptions = mkOption { 181 type = types.listOf types.str; 182 default = [ ]; 183 description = lib.mdDoc '' 184 Extra extended options to be passed to the restic --option flag. 185 ''; 186 example = [ 187 "sftp.command='ssh backup@192.168.1.100 -i /home/user/.ssh/id_rsa -s sftp'" 188 ]; 189 }; 190 191 initialize = mkOption { 192 type = types.bool; 193 default = false; 194 description = lib.mdDoc '' 195 Create the repository if it doesn't exist. 196 ''; 197 }; 198 199 pruneOpts = mkOption { 200 type = types.listOf types.str; 201 default = [ ]; 202 description = lib.mdDoc '' 203 A list of options (--keep-\* et al.) for 'restic forget 204 --prune', to automatically prune old snapshots. The 205 'forget' command is run *after* the 'backup' command, so 206 keep that in mind when constructing the --keep-\* options. 207 ''; 208 example = [ 209 "--keep-daily 7" 210 "--keep-weekly 5" 211 "--keep-monthly 12" 212 "--keep-yearly 75" 213 ]; 214 }; 215 216 checkOpts = mkOption { 217 type = types.listOf types.str; 218 default = [ ]; 219 description = lib.mdDoc '' 220 A list of options for 'restic check', which is run after 221 pruning. 222 ''; 223 example = [ 224 "--with-cache" 225 ]; 226 }; 227 228 dynamicFilesFrom = mkOption { 229 type = with types; nullOr str; 230 default = null; 231 description = lib.mdDoc '' 232 A script that produces a list of files to back up. The 233 results of this command are given to the '--files-from' 234 option. 235 ''; 236 example = "find /home/matt/git -type d -name .git"; 237 }; 238 239 backupPrepareCommand = mkOption { 240 type = with types; nullOr str; 241 default = null; 242 description = lib.mdDoc '' 243 A script that must run before starting the backup process. 244 ''; 245 }; 246 247 backupCleanupCommand = mkOption { 248 type = with types; nullOr str; 249 default = null; 250 description = lib.mdDoc '' 251 A script that must run after finishing the backup process. 252 ''; 253 }; 254 255 package = mkOption { 256 type = types.package; 257 default = pkgs.restic; 258 defaultText = literalExpression "pkgs.restic"; 259 description = lib.mdDoc '' 260 Restic package to use. 261 ''; 262 }; 263 }; 264 })); 265 default = { }; 266 example = { 267 localbackup = { 268 paths = [ "/home" ]; 269 exclude = [ "/home/*/.cache" ]; 270 repository = "/mnt/backup-hdd"; 271 passwordFile = "/etc/nixos/secrets/restic-password"; 272 initialize = true; 273 }; 274 remotebackup = { 275 paths = [ "/home" ]; 276 repository = "sftp:backup@host:/backups/home"; 277 passwordFile = "/etc/nixos/secrets/restic-password"; 278 extraOptions = [ 279 "sftp.command='ssh backup@host -i /etc/nixos/secrets/backup-private-key -s sftp'" 280 ]; 281 timerConfig = { 282 OnCalendar = "00:05"; 283 RandomizedDelaySec = "5h"; 284 }; 285 }; 286 }; 287 }; 288 289 config = { 290 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); 291 assertions = mapAttrsToList (n: v: { 292 assertion = (v.repository == null) != (v.repositoryFile == null); 293 message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set"; 294 }) config.services.restic.backups; 295 systemd.services = 296 mapAttrs' 297 (name: backup: 298 let 299 extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions; 300 resticCmd = "${backup.package}/bin/restic${extraOptions}"; 301 excludeFlags = if (backup.exclude != []) then ["--exclude-file=${pkgs.writeText "exclude-patterns" (concatStringsSep "\n" backup.exclude)}"] else []; 302 filesFromTmpFile = "/run/restic-backups-${name}/includes"; 303 backupPaths = 304 if (backup.dynamicFilesFrom == null) 305 then optionalString (backup.paths != null) (concatStringsSep " " backup.paths) 306 else "--files-from ${filesFromTmpFile}"; 307 pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ 308 (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts)) 309 (resticCmd + " check " + (concatStringsSep " " backup.checkOpts)) 310 ]; 311 # Helper functions for rclone remotes 312 rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1; 313 rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); 314 rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); 315 toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; 316 in 317 nameValuePair "restic-backups-${name}" ({ 318 environment = { 319 RESTIC_CACHE_DIR = "%C/restic-backups-${name}"; 320 RESTIC_PASSWORD_FILE = backup.passwordFile; 321 RESTIC_REPOSITORY = backup.repository; 322 RESTIC_REPOSITORY_FILE = backup.repositoryFile; 323 } // optionalAttrs (backup.rcloneOptions != null) (mapAttrs' 324 (name: value: 325 nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) 326 ) 327 backup.rcloneOptions) // optionalAttrs (backup.rcloneConfigFile != null) { 328 RCLONE_CONFIG = backup.rcloneConfigFile; 329 } // optionalAttrs (backup.rcloneConfig != null) (mapAttrs' 330 (name: value: 331 nameValuePair (rcloneAttrToConf name) (toRcloneVal value) 332 ) 333 backup.rcloneConfig); 334 path = [ pkgs.openssh ]; 335 restartIfChanged = false; 336 serviceConfig = { 337 Type = "oneshot"; 338 ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup ${concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} ${backupPaths}" ]) 339 ++ pruneCmd; 340 User = backup.user; 341 RuntimeDirectory = "restic-backups-${name}"; 342 CacheDirectory = "restic-backups-${name}"; 343 CacheDirectoryMode = "0700"; 344 PrivateTmp = true; 345 } // optionalAttrs (backup.environmentFile != null) { 346 EnvironmentFile = backup.environmentFile; 347 }; 348 } // optionalAttrs (backup.initialize || backup.dynamicFilesFrom != null || backup.backupPrepareCommand != null) { 349 preStart = '' 350 ${optionalString (backup.backupPrepareCommand != null) '' 351 ${pkgs.writeScript "backupPrepareCommand" backup.backupPrepareCommand} 352 ''} 353 ${optionalString (backup.initialize) '' 354 ${resticCmd} snapshots || ${resticCmd} init 355 ''} 356 ${optionalString (backup.dynamicFilesFrom != null) '' 357 ${pkgs.writeScript "dynamicFilesFromScript" backup.dynamicFilesFrom} > ${filesFromTmpFile} 358 ''} 359 ''; 360 } // optionalAttrs (backup.dynamicFilesFrom != null || backup.backupCleanupCommand != null) { 361 postStop = '' 362 ${optionalString (backup.backupCleanupCommand != null) '' 363 ${pkgs.writeScript "backupCleanupCommand" backup.backupCleanupCommand} 364 ''} 365 ${optionalString (backup.dynamicFilesFrom != null) '' 366 rm ${filesFromTmpFile} 367 ''} 368 ''; 369 }) 370 ) 371 config.services.restic.backups; 372 systemd.timers = 373 mapAttrs' 374 (name: backup: nameValuePair "restic-backups-${name}" { 375 wantedBy = [ "timers.target" ]; 376 timerConfig = backup.timerConfig; 377 }) 378 config.services.restic.backups; 379 }; 380}