diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix index adabb2ce0f8b..2512f4643c68 100644 --- a/nixos/modules/services/backup/borgbackup.nix +++ b/nixos/modules/services/backup/borgbackup.nix @@ -89,19 +89,24 @@ let ${cfg.postInit} fi '' - + '' - ( - set -o pipefail - ${lib.optionalString (cfg.dumpCommand != null) ''${lib.escapeShellArg cfg.dumpCommand} | \''} - borgWrapper create "''${extraArgs[@]}" \ - --compression ${cfg.compression} \ - --exclude-from ${mkExcludeFile cfg} \ - --patterns-from ${mkPatternsFile cfg} \ - "''${extraCreateArgs[@]}" \ - "::$archiveName$archiveSuffix" \ - ${if cfg.paths == null then "-" else lib.escapeShellArgs cfg.paths} - ) - '' + + ( + let + import-tar = cfg.createCommand == "import-tar"; + in + '' + ( + set -o pipefail + ${lib.optionalString (cfg.dumpCommand != null) ''${lib.escapeShellArg cfg.dumpCommand} | \''} + borgWrapper ${lib.escapeShellArg cfg.createCommand} "''${extraArgs[@]}" \ + --compression ${cfg.compression} \ + ${lib.optionalString (!import-tar) "--exclude-from ${mkExcludeFile cfg}"} \ + ${lib.optionalString (!import-tar) "--patterns-from ${mkPatternsFile cfg}"} \ + "''${extraCreateArgs[@]}" \ + "::$archiveName$archiveSuffix" \ + ${if cfg.paths == null then "-" else lib.escapeShellArgs cfg.paths} + ) + '' + ) + lib.optionalString cfg.appendFailedSuffix '' borgWrapper rename "''${extraArgs[@]}" \ "::$archiveName$archiveSuffix" "$archiveName" @@ -310,6 +315,23 @@ let ''; }; + mkCreateCommandImportTarDumpCommandAssertion = name: cfg: { + assertion = cfg.createCommand != "import-tar" || cfg.dumpCommand != null; + message = '' + Option borgbackup.jobs.${name}.dumpCommand is required when createCommand + is set to "import-tar". + ''; + }; + + mkCreateCommandImportTarExclusionsAssertion = name: cfg: { + assertion = cfg.createCommand != "import-tar" || (cfg.exclude == [ ] && cfg.patterns == [ ]); + message = '' + Options borgbackup.jobs.${name}.exclude and + borgbackup.jobs.${name}.patterns have no effect when createCommand is set + to "import-tar". + ''; + }; + mkRemovableDeviceAssertions = name: cfg: { assertion = !(isLocalPath cfg.repo) -> !cfg.removableDevice; message = '' @@ -377,6 +399,25 @@ in { name, config, ... }: { options = { + createCommand = lib.mkOption { + type = lib.types.enum [ + "create" + "import-tar" + ]; + description = '' + Borg command to use for archive creation. The default (`create`) + creates a regular Borg archive. + + Use `import-tar` to instead read a tar archive stream from + {option}`dumpCommand` output and import its contents into the + repository. + + `import-tar` can not be used together with {option}`exclude` or + {option}`patterns`. + ''; + default = "create"; + example = "import-tar"; + }; paths = lib.mkOption { type = with lib.types; nullOr (coercedTo str lib.singleton (listOf str)); @@ -556,6 +597,9 @@ in description = '' Exclude paths matching any of the given patterns. See {command}`borg help patterns` for pattern syntax. + + Can not be set when {option}`createCommand` is set to + `import-tar`. ''; default = [ ]; example = [ @@ -571,6 +615,9 @@ in matching patterns is used, so if an include pattern (prefix `+`) matches before an exclude pattern (prefix `-`), the file is backed up. See [{command}`borg help patterns`](https://borgbackup.readthedocs.io/en/stable/usage/help.html#borg-patterns) for pattern syntax. + + Can not be set when {option}`createCommand` is set to + `import-tar`. ''; default = [ ]; example = [ @@ -888,6 +935,8 @@ in lib.mapAttrsToList mkPassAssertion jobs ++ lib.mapAttrsToList mkKeysAssertion repos ++ lib.mapAttrsToList mkSourceAssertions jobs + ++ lib.mapAttrsToList mkCreateCommandImportTarDumpCommandAssertion jobs + ++ lib.mapAttrsToList mkCreateCommandImportTarExclusionsAssertion jobs ++ lib.mapAttrsToList mkRemovableDeviceAssertions jobs; systemd.tmpfiles.settings = lib.mapAttrs' mkTmpfiles jobs; diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix index ae9f4f51bef5..ed965600d277 100644 --- a/nixos/tests/borgbackup.nix +++ b/nixos/tests/borgbackup.nix @@ -9,6 +9,7 @@ let keepFile = "important_file"; keepFileData = "important_data"; localRepo = "/root/back:up"; + localTarRepo = "/root/backup-tar"; # a repository on a file system which is not mounted automatically localRepoMount = "/noAutoMount"; archiveName = "my_archive"; @@ -47,7 +48,7 @@ in nodes = { client = - { ... }: + { lib, pkgs, ... }: { virtualisation.fileSystems.${localRepoMount} = { device = "tmpfs"; @@ -93,6 +94,20 @@ in startAt = [ ]; }; + localTar = { + dumpCommand = pkgs.writeScript "createTarArchive" '' + ${lib.getExe pkgs.gnutar} cf - ${dataDir} + ''; + createCommand = "import-tar"; + repo = localTarRepo; + # Make sure in import-tar mode encryption flags are still respected. + encryption = { + mode = "repokey"; + inherit passphrase; + }; + startAt = [ ]; # Do not run automatically + }; + remote = { paths = dataDir; repo = remoteRepo; @@ -231,6 +246,19 @@ in # Make sure disabling wrapper works client.fail("command -v borg-job-localMount") + with subtest("localTar"): + borg = "BORG_PASSPHRASE='${passphrase}' borg" + client.systemctl("start --wait borgbackup-job-localTar") + client.fail("systemctl is-failed borgbackup-job-localTar") + archiveName, = client.succeed("{} list --format '{{archive}}{{NL}}' '${localTarRepo}'".format(borg)).strip().split("\n") + # Since excludes are not supported by import-tar, we expect to find exclude file, too + client.succeed( + "{} list '${localTarRepo}::{}' | grep -qF '${excludeFile}'".format(borg, archiveName) + ) + # Make sure keepFile has the correct content + client.succeed("{} extract '${localTarRepo}::{}'".format(borg, archiveName)) + assert "${keepFileData}" in client.succeed("cat ${dataDir}/${keepFile}") + with subtest("remote"): borg = "BORG_RSH='ssh -oStrictHostKeyChecking=no -i /root/id_ed25519' borg" server.wait_for_unit("sshd.service")