nixos/borgbackup: allow dump scripts as stdin inputs

borg is able to process stdin during backups when backing up the special path -,
which can be very useful for backing up things that can be streamed (eg database
dumps, zfs snapshots).

authored by

pennae and committed by
tomberek
1fa5e13f c47fcb70

+68 -8
+35 -8
nixos/modules/services/backup/borgbackup.nix
··· 42 ${cfg.postInit} 43 fi 44 '' + '' 45 - borg create $extraArgs \ 46 - --compression ${cfg.compression} \ 47 - --exclude-from ${mkExcludeFile cfg} \ 48 - $extraCreateArgs \ 49 - "::$archiveName$archiveSuffix" \ 50 - ${escapeShellArgs cfg.paths} 51 '' + optionalString cfg.appendFailedSuffix '' 52 borg rename $extraArgs \ 53 "::$archiveName$archiveSuffix" "$archiveName" ··· 182 + " without at least one public key"; 183 }; 184 185 mkRemovableDeviceAssertions = name: cfg: { 186 assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice; 187 message = '' ··· 240 options = { 241 242 paths = mkOption { 243 - type = with types; coercedTo str lib.singleton (listOf str); 244 - description = "Path(s) to back up."; 245 example = "/home/user"; 246 }; 247 248 repo = mkOption { ··· 657 assertions = 658 mapAttrsToList mkPassAssertion jobs 659 ++ mapAttrsToList mkKeysAssertion repos 660 ++ mapAttrsToList mkRemovableDeviceAssertions jobs; 661 662 system.activationScripts = mapAttrs' mkActivationScript jobs;
··· 42 ${cfg.postInit} 43 fi 44 '' + '' 45 + ( 46 + set -o pipefail 47 + ${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''} 48 + borg create $extraArgs \ 49 + --compression ${cfg.compression} \ 50 + --exclude-from ${mkExcludeFile cfg} \ 51 + $extraCreateArgs \ 52 + "::$archiveName$archiveSuffix" \ 53 + ${if cfg.paths == null then "-" else escapeShellArgs cfg.paths} 54 + ) 55 '' + optionalString cfg.appendFailedSuffix '' 56 borg rename $extraArgs \ 57 "::$archiveName$archiveSuffix" "$archiveName" ··· 186 + " without at least one public key"; 187 }; 188 189 + mkSourceAssertions = name: cfg: { 190 + assertion = count isNull [ cfg.dumpCommand cfg.paths ] == 1; 191 + message = '' 192 + Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand 193 + must be set. 194 + ''; 195 + }; 196 + 197 mkRemovableDeviceAssertions = name: cfg: { 198 assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice; 199 message = '' ··· 252 options = { 253 254 paths = mkOption { 255 + type = with types; nullOr (coercedTo str lib.singleton (listOf str)); 256 + default = null; 257 + description = '' 258 + Path(s) to back up. 259 + Mutually exclusive with <option>dumpCommand</option>. 260 + ''; 261 example = "/home/user"; 262 + }; 263 + 264 + dumpCommand = mkOption { 265 + type = with types; nullOr path; 266 + default = null; 267 + description = '' 268 + Backup the stdout of this program instead of filesystem paths. 269 + Mutually exclusive with <option>paths</option>. 270 + ''; 271 + example = "/path/to/createZFSsend.sh"; 272 }; 273 274 repo = mkOption { ··· 683 assertions = 684 mapAttrsToList mkPassAssertion jobs 685 ++ mapAttrsToList mkKeysAssertion repos 686 + ++ mapAttrsToList mkSourceAssertions jobs 687 ++ mapAttrsToList mkRemovableDeviceAssertions jobs; 688 689 system.activationScripts = mapAttrs' mkActivationScript jobs;
+33
nixos/tests/borgbackup.nix
··· 81 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly"; 82 }; 83 84 }; 85 }; 86 ··· 171 client.fail("{} list borg\@server:wrong".format(borg)) 172 173 # TODO: Make sure that data is not actually deleted 174 ''; 175 })
··· 81 environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly"; 82 }; 83 84 + commandSuccess = { 85 + dumpCommand = pkgs.writeScript "commandSuccess" '' 86 + echo -n test 87 + ''; 88 + repo = remoteRepo; 89 + encryption.mode = "none"; 90 + startAt = [ ]; 91 + environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519"; 92 + }; 93 + 94 + commandFail = { 95 + dumpCommand = "${pkgs.coreutils}/bin/false"; 96 + repo = remoteRepo; 97 + encryption.mode = "none"; 98 + startAt = [ ]; 99 + environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519"; 100 + }; 101 + 102 }; 103 }; 104 ··· 189 client.fail("{} list borg\@server:wrong".format(borg)) 190 191 # TODO: Make sure that data is not actually deleted 192 + 193 + with subtest("commandSuccess"): 194 + server.wait_for_unit("sshd.service") 195 + client.wait_for_unit("network.target") 196 + client.systemctl("start --wait borgbackup-job-commandSuccess") 197 + client.fail("systemctl is-failed borgbackup-job-commandSuccess") 198 + id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip() 199 + client.succeed(f"borg-job-commandSuccess extract ::{id} stdin") 200 + assert "test" == client.succeed("cat stdin") 201 + 202 + with subtest("commandFail"): 203 + server.wait_for_unit("sshd.service") 204 + client.wait_for_unit("network.target") 205 + client.systemctl("start --wait borgbackup-job-commandFail") 206 + client.succeed("systemctl is-failed borgbackup-job-commandFail") 207 ''; 208 })