diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index 5461dbaf0bd0..220c571b927e 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -42,12 +42,16 @@ let
${cfg.postInit}
fi
'' + ''
- borg create $extraArgs \
- --compression ${cfg.compression} \
- --exclude-from ${mkExcludeFile cfg} \
- $extraCreateArgs \
- "::$archiveName$archiveSuffix" \
- ${escapeShellArgs cfg.paths}
+ (
+ set -o pipefail
+ ${optionalString (cfg.dumpCommand != null) ''${escapeShellArg cfg.dumpCommand} | \''}
+ borg create $extraArgs \
+ --compression ${cfg.compression} \
+ --exclude-from ${mkExcludeFile cfg} \
+ $extraCreateArgs \
+ "::$archiveName$archiveSuffix" \
+ ${if cfg.paths == null then "-" else escapeShellArgs cfg.paths}
+ )
'' + optionalString cfg.appendFailedSuffix ''
borg rename $extraArgs \
"::$archiveName$archiveSuffix" "$archiveName"
@@ -182,6 +186,14 @@ let
+ " without at least one public key";
};
+ mkSourceAssertions = name: cfg: {
+ assertion = count isNull [ cfg.dumpCommand cfg.paths ] == 1;
+ message = ''
+ Exactly one of borgbackup.jobs.${name}.paths or borgbackup.jobs.${name}.dumpCommand
+ must be set.
+ '';
+ };
+
mkRemovableDeviceAssertions = name: cfg: {
assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice;
message = ''
@@ -240,11 +252,25 @@ in {
options = {
paths = mkOption {
- type = with types; coercedTo str lib.singleton (listOf str);
- description = "Path(s) to back up.";
+ type = with types; nullOr (coercedTo str lib.singleton (listOf str));
+ default = null;
+ description = ''
+ Path(s) to back up.
+ Mutually exclusive with .
+ '';
example = "/home/user";
};
+ dumpCommand = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ description = ''
+ Backup the stdout of this program instead of filesystem paths.
+ Mutually exclusive with .
+ '';
+ example = "/path/to/createZFSsend.sh";
+ };
+
repo = mkOption {
type = types.str;
description = "Remote or local repository to back up to.";
@@ -657,6 +683,7 @@ in {
assertions =
mapAttrsToList mkPassAssertion jobs
++ mapAttrsToList mkKeysAssertion repos
+ ++ mapAttrsToList mkSourceAssertions jobs
++ mapAttrsToList mkRemovableDeviceAssertions jobs;
system.activationScripts = mapAttrs' mkActivationScript jobs;
diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix
index fae1d2d07138..cbb28689209b 100644
--- a/nixos/tests/borgbackup.nix
+++ b/nixos/tests/borgbackup.nix
@@ -81,6 +81,24 @@ in {
environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519.appendOnly";
};
+ commandSuccess = {
+ dumpCommand = pkgs.writeScript "commandSuccess" ''
+ echo -n test
+ '';
+ repo = remoteRepo;
+ encryption.mode = "none";
+ startAt = [ ];
+ environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
+ };
+
+ commandFail = {
+ dumpCommand = "${pkgs.coreutils}/bin/false";
+ repo = remoteRepo;
+ encryption.mode = "none";
+ startAt = [ ];
+ environment.BORG_RSH = "ssh -oStrictHostKeyChecking=no -i /root/id_ed25519";
+ };
+
};
};
@@ -171,5 +189,20 @@ in {
client.fail("{} list borg\@server:wrong".format(borg))
# TODO: Make sure that data is not actually deleted
+
+ with subtest("commandSuccess"):
+ server.wait_for_unit("sshd.service")
+ client.wait_for_unit("network.target")
+ client.systemctl("start --wait borgbackup-job-commandSuccess")
+ client.fail("systemctl is-failed borgbackup-job-commandSuccess")
+ id = client.succeed("borg-job-commandSuccess list | tail -n1 | cut -d' ' -f1").strip()
+ client.succeed(f"borg-job-commandSuccess extract ::{id} stdin")
+ assert "test" == client.succeed("cat stdin")
+
+ with subtest("commandFail"):
+ server.wait_for_unit("sshd.service")
+ client.wait_for_unit("network.target")
+ client.systemctl("start --wait borgbackup-job-commandFail")
+ client.succeed("systemctl is-failed borgbackup-job-commandFail")
'';
})