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