Compare commits

...

53 commits

Author SHA1 Message Date
e7b7560477 Bump version for release. 2022-08-21 21:54:13 -07:00
317dc7fbce Add "before_actions" and "after_actions" command hooks that run before/after all the actions for each repository, update docs to cover per-repository configurations (#463). 2022-08-21 21:48:37 -07:00
97fad15009 Switch to more accessible header permalink anchors in documentation. 2022-08-21 21:48:07 -07:00
462326406e Drop only-style actions like "--create", rename "prune --files" to "prune --list", and add "--list" alias to "create" and "export-tar" (#571). 2022-08-21 14:25:16 -07:00
bbdf4893d1 Clarify --format flag in documentation. 2022-08-19 15:27:03 -07:00
ef6617cfe6
Add link to Borg list --format documentation. 2022-08-19 15:16:56 -07:00
dbef0a440f
Merge branch 'master' into patch-2 2022-08-19 15:16:17 -07:00
22628ba5d4 Update ssh:// examples in documentation to use relative paths on the remote machine (#557). 2022-08-19 12:00:40 -07:00
8576ac86b9 Fix incorrect version in documentation (#557). 2022-08-19 09:44:31 -07:00
540f9f6b72 Add missing test for "transfer" action (#557). 2022-08-19 09:40:29 -07:00
f9d7faf884 Fix mount action to work without archive again (#557). 2022-08-18 23:33:05 -07:00
7dee6194a2 Add new "transfer" action for Borg 2 (#557). 2022-08-18 23:06:51 -07:00
68f9c1b950 Add generate-borgmatic-config end-to-end test. 2022-08-18 14:28:46 -07:00
43d711463c Add additional command-line flags to rcreate action (#557). 2022-08-18 14:28:12 -07:00
00255a2437 Various documentation edits for Borg 2 (#557). 2022-08-18 10:19:11 -07:00
b40e9b7da2 Ignore archive filter parameters passed to list action when --archive is given (#557). 2022-08-18 09:59:48 -07:00
89d201c8ff Fleshing out NEWS for the Borg 2 changes. 2022-08-17 21:54:00 -07:00
f47c98c4a5 Rename several configuration options to match Borg 2 (#557). 2022-08-17 21:14:58 -07:00
3b6ed06686 Add --other-repo flag to rcreate action (#557). 2022-08-17 17:33:09 -07:00
57009e22b5 Use flag-related utility functions in info action (#557). 2022-08-17 17:11:02 -07:00
3ab7a3b64a Replace use of --prefix with --glob-archives in info action (#557). 2022-08-17 15:36:19 -07:00
596dd49cf5 Use --glob-archives instead of --prefix on rlist command (#557). 2022-08-17 14:26:35 -07:00
28d847b8b1 Warn and tranform on non-ssh://-style repositories (#557). 2022-08-17 10:13:11 -07:00
2a1c6b1477 Update documentation with newly required ssh:// repository syntax for Borg 2 (#557). 2022-08-16 11:41:35 -07:00
30abd0e3de Update borg action for Borg 2 support (#557). 2022-08-16 09:30:00 -07:00
f36e38ec20 Update mount action for Borg 2 support (#557). 2022-08-15 19:32:37 -07:00
d807ce095e Update export-tar action for Borg 2 support (#557). 2022-08-15 17:34:12 -07:00
7626fe1189 Disallow borg list --json with --archive or --find (#557). 2022-08-15 15:40:28 -07:00
cc04bf57df Update list action for Borg 2 support, add rinfo action, and update extract consistency check for Borg 2. 2022-08-15 15:04:40 -07:00
cce6d56661 Update extract action for Borg 2 support (#557). 2022-08-13 23:07:29 -07:00
a05d0f378e Factor out repository/archive flags formatting code from create action (#557). 2022-08-13 22:50:14 -07:00
94321aec7a Update compact action for Borg 2 support (#557). 2022-08-13 22:07:15 -07:00
4a55749bd2 Update prune action for Borg 2 support (#557). 2022-08-13 17:26:51 -07:00
2898e63166 Update create action for Borg 2 support (#557). 2022-08-12 23:54:13 -07:00
c7176bd00a Add rinfo action for Borg 2 support (#557). 2022-08-12 23:06:56 -07:00
647ecdac29 Borg 2 support in borgmatic check action (#557). 2022-08-12 15:46:33 -07:00
e7a8acfb96 Add missing rinfo action source files (#557). 2022-08-12 14:59:03 -07:00
622caa0c21 Support for Borg 2's rcreate and rinfo sub-commands (#557). 2022-08-12 14:53:20 -07:00
22149c6401 Switch to self-hosted container registry for borgmatic documentation image. 2022-08-01 21:17:59 -07:00
9aece3936a Modify "mount" and "extract" actions to require the "--repository" flag when multiple repositories are configured (#566). 2022-07-25 11:30:02 -07:00
c7e4e6f6c9 Add Healthchecks "verify_tls" option to NEWS. 2022-07-23 23:16:06 -07:00
bcad0de1a4
Add verify_tls option for Healthchecks to optionally disable TLS verification. 2022-07-23 23:11:41 -07:00
Uli
5c6407047f feat: add verify_tls flag for Healthchecks 2022-07-24 07:37:00 +02:00
6ddae20fa1 Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags (#565). 2022-07-23 21:02:21 -07:00
23feac2f4c Bump version for release. 2022-07-19 20:32:41 -07:00
16066942e3 Fix traceback with "create" action and "--json" flag when a database hook is configured (#563). 2022-07-19 10:25:10 -07:00
Jelle @ Samson-IT
f7c8e89a9f
update format specifier syntax link to use anchor 2022-07-13 21:52:21 +02:00
Jelle @ Samson-IT
ba377952fd
Added link to borgbackup list --format docs
I kept searching for this link, so it's time to add it to official docs.
2022-07-13 13:52:48 +02:00
e85d551eac Fix all database hooks to error when the requested database to restore isn't present in the Borg archive (#560). 2022-07-06 23:21:24 -07:00
2b23a63a08 Add end-to-end test for overrides. 2022-07-06 18:20:51 -07:00
c0f48e1071 Fix command-line "--override" flag to continue supporting old configuration file formats (#561). 2022-07-06 18:14:44 -07:00
6005426684 Update documentation about configuring multiple consistency checks or multiple databases (#559). 2022-07-03 22:24:25 -07:00
673ed1a2d3 Clarify check frequency documentation in regards to multiple configuration files. 2022-07-02 09:40:49 -07:00
78 changed files with 4185 additions and 1150 deletions

View file

@ -42,7 +42,9 @@ steps:
from_secret: docker_username
password:
from_secret: docker_password
repo: witten/borgmatic-docs
registry: projects.torsion.org
repo: projects.torsion.org/borgmatic-collective/borgmatic
tags: docs
dockerfile: docs/Dockerfile
trigger:

View file

@ -23,8 +23,7 @@ module.exports = function(eleventyConfig) {
}
};
let markdownItAnchorOptions = {
permalink: true,
permalinkClass: "direct-link"
permalink: markdownItAnchor.permalink.headerLink()
};
eleventyConfig.setLibrary(

43
NEWS
View file

@ -1,3 +1,46 @@
1.7.0
* #463: Add "before_actions" and "after_actions" command hooks that run before/after all the
actions for each repository. These new hooks are a good place to run per-repository steps like
mounting/unmounting a remote filesystem.
* #463: Update documentation to cover per-repository configurations:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/
* #557: Support for Borg 2 while still working with Borg 1. This includes new borgmatic actions
like "rcreate" (replaces "init"), "rlist" (list archives in repository), "rinfo" (show repository
info), and "transfer" (for upgrading Borg repositories). For the most part, borgmatic tries to
smooth over differences between Borg 1 and 2 to make your upgrade process easier. However, there
are still a few cases where Borg made breaking changes. See the Borg 2.0 changelog for more
information: https://www.borgbackup.org/releases/borg-2.0.html
* #557: If you install Borg 2, you'll need to manually upgrade your existing Borg 1 repositories
before use. Note that Borg 2 stable is not yet released as of this borgmatic release, so don't
use Borg 2 for production until it is! See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-borg
* #557: Rename several configuration options to match Borg 2: "remote_rate_limit" is now
"upload_rate_limit", "numeric_owner" is "numeric_ids", and "bsd_flags" is "flags". borgmatic
still works with the old options.
* #557: Remote repository paths without the "ssh://" syntax are deprecated but still supported for
now. Remote repository paths containing "~" are deprecated in borgmatic and no longer work in
Borg 2.
* #557: Omitting the "--archive" flag on the "list" action is deprecated when using Borg 2. Use
the new "rlist" action instead.
* #557: The "--dry-run" flag can now be used with the "rcreate"/"init" action.
* #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags.
* #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple
repositories are configured.
* #571: BREAKING: Remove old-style command-line action flags like "--create, "--list", etc. If
you're already using actions like "create" and "list" instead, this change should not affect you.
* #571: BREAKING: Rename "--files" flag on "prune" action to "--list", as it lists archives, not
files.
* #571: Add "--list" as alias for "--files" flag on "create" and "export-tar" actions.
* Add support for disabling TLS verification in Healthchecks monitoring hook with "verify_tls"
option.
1.6.6
* #559: Update documentation about configuring multiple consistency checks or multiple databases.
* #560: Fix all database hooks to error when the requested database to restore isn't present in the
Borg archive.
* #561: Fix command-line "--override" flag to continue supporting old configuration file formats.
* #563: Fix traceback with "create" action and "--json" flag when a database hook is configured.
1.6.5
* #553: Fix logging to include the full traceback when Borg experiences an internal error, not just
the first few lines.

View file

@ -24,8 +24,8 @@ location:
# Paths of local or remote repositories to backup to.
repositories:
- 1234@usw-s001.rsync.net:backups.borg
- k8pDxu32@k8pDxu32.repo.borgbase.com:repo
- ssh://1234@usw-s001.rsync.net/./backups.borg
- ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
- /var/lib/backups/local.borg
retention:

View file

@ -1,24 +1,29 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg.flags import make_flags
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
REPOSITORYLESS_BORG_COMMANDS = {'serve', None}
BORG_COMMANDS_WITH_SUBCOMMANDS = {'key', 'debug'}
BORG_SUBCOMMANDS_WITHOUT_REPOSITORY = (('debug', 'info'), ('debug', 'convert-profile'))
BORG_SUBCOMMANDS_WITH_SUBCOMMANDS = {'key', 'debug'}
BORG_SUBCOMMANDS_WITHOUT_REPOSITORY = (('debug', 'info'), ('debug', 'convert-profile'), ())
def run_arbitrary_borg(
repository, storage_config, options, archive=None, local_path='borg', remote_path=None
repository,
storage_config,
local_borg_version,
options,
archive=None,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, a sequence of arbitrary
command-line Borg options, and an optional archive name, run an arbitrary Borg command on the
given repository/archive.
Given a local or remote repository path, a storage config dict, the local Borg version, a
sequence of arbitrary command-line Borg options, and an optional archive name, run an arbitrary
Borg command on the given repository/archive.
'''
lock_wait = storage_config.get('lock_wait', None)
@ -26,7 +31,7 @@ def run_arbitrary_borg(
options = options[1:] if options[0] == '--' else options
# Borg commands like "key" have a sub-command ("export", etc.) that must follow it.
command_options_start_index = 2 if options[0] in BORG_COMMANDS_WITH_SUBCOMMANDS else 1
command_options_start_index = 2 if options[0] in BORG_SUBCOMMANDS_WITH_SUBCOMMANDS else 1
borg_command = tuple(options[:command_options_start_index])
command_options = tuple(options[command_options_start_index:])
except IndexError:
@ -34,21 +39,23 @@ def run_arbitrary_borg(
command_options = ()
if borg_command in BORG_SUBCOMMANDS_WITHOUT_REPOSITORY:
repository_archive = None
else:
repository_archive = (
'::'.join((repository, archive)) if repository and archive else repository
repository_archive_flags = ()
elif archive:
repository_archive_flags = flags.make_repository_archive_flags(
repository, archive, local_borg_version
)
else:
repository_archive_flags = flags.make_repository_flags(repository, local_borg_version)
full_command = (
(local_path,)
+ borg_command
+ ((repository_archive,) if borg_command and repository_archive else ())
+ repository_archive_flags
+ command_options
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
)
return execute_command(

View file

@ -5,7 +5,7 @@ import logging
import os
import pathlib
from borgmatic.borg import environment, extract, info, state
from borgmatic.borg import environment, extract, flags, rinfo, state
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
DEFAULT_CHECKS = (
@ -33,8 +33,6 @@ def parse_checks(consistency_config, only_checks=None):
If no "checks" option is present in the config, return the DEFAULT_CHECKS. If a checks value
has a name of "disabled", return an empty tuple, meaning that no checks should be run.
If the "data" check is present, then make sure the "archives" check is included as well.
'''
checks = only_checks or tuple(
check_config['name']
@ -48,9 +46,6 @@ def parse_checks(consistency_config, only_checks=None):
)
return ()
if 'data' in checks and 'archives' not in checks:
return checks + ('archives',)
return checks
@ -164,18 +159,18 @@ def make_check_flags(checks, check_last=None, prefix=None):
('--repository-only',)
However, if both "repository" and "archives" are in checks, then omit them from the returned
flags because Borg does both checks by default.
flags because Borg does both checks by default. If "data" is in checks, that implies "archives".
Additionally, if a check_last value is given and "archives" is in checks, then include a
"--last" flag. And if a prefix value is given and "archives" is in checks, then include a
"--prefix" flag.
"--glob-archives" flag.
'''
if 'archives' in checks:
last_flags = ('--last', str(check_last)) if check_last else ()
prefix_flags = ('--prefix', prefix) if prefix else ()
glob_archives_flags = ('--glob-archives', f'{prefix}*') if prefix else ()
else:
last_flags = ()
prefix_flags = ()
glob_archives_flags = ()
if check_last:
logger.info('Ignoring check_last option, as "archives" is not in consistency checks')
if prefix:
@ -183,7 +178,13 @@ def make_check_flags(checks, check_last=None, prefix=None):
'Ignoring consistency prefix option, as "archives" is not in consistency checks'
)
common_flags = last_flags + prefix_flags + (('--verify-data',) if 'data' in checks else ())
if 'data' in checks:
data_flags = ('--verify-data',)
checks += ('archives',)
else:
data_flags = ()
common_flags = last_flags + glob_archives_flags + data_flags
if {'repository', 'archives'}.issubset(set(checks)):
return common_flags
@ -240,6 +241,7 @@ def check_archives(
location_config,
storage_config,
consistency_config,
local_borg_version,
local_path='borg',
remote_path=None,
progress=None,
@ -259,10 +261,11 @@ def check_archives(
'''
try:
borg_repository_id = json.loads(
info.display_archives_info(
rinfo.display_repository_info(
repository,
storage_config,
argparse.Namespace(json=True, archive=None),
local_borg_version,
argparse.Namespace(json=True),
local_path,
remote_path,
)
@ -301,7 +304,7 @@ def check_archives(
+ verbosity_flags
+ (('--progress',) if progress else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
+ flags.make_repository_flags(repository, local_borg_version)
)
borg_environment = environment.make_environment(storage_config)
@ -320,6 +323,6 @@ def check_archives(
if 'extract' in checks:
extract.extract_last_archive_dry_run(
storage_config, repository, lock_wait, local_path, remote_path
storage_config, local_borg_version, repository, lock_wait, local_path, remote_path
)
write_check_time(make_check_time_path(location_config, borg_repository_id, 'extract'))

View file

@ -1,6 +1,6 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@ -10,6 +10,7 @@ def compact_segments(
dry_run,
repository,
storage_config,
local_borg_version,
local_path='borg',
remote_path=None,
progress=False,
@ -17,8 +18,8 @@ def compact_segments(
threshold=None,
):
'''
Given dry-run flag, a local or remote repository path, and a storage config dict, compact Borg
segments in a repository.
Given dry-run flag, a local or remote repository path, a storage config dict, and the local
Borg version, compact the segments in a repository.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
@ -35,13 +36,16 @@ def compact_segments(
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
+ flags.make_repository_flags(repository, local_borg_version)
)
if not dry_run:
execute_command(
full_command,
output_log_level=logging.INFO,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)
if dry_run:
logging.info(f'{repository}: Skipping compact (dry run)')
return
execute_command(
full_command,
output_log_level=logging.INFO,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)

View file

@ -5,7 +5,7 @@ import os
import pathlib
import tempfile
from borgmatic.borg import environment, feature, state
from borgmatic.borg import environment, feature, flags, state
from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes
logger = logging.getLogger(__name__)
@ -203,7 +203,7 @@ def create_archive(
progress=False,
stats=False,
json=False,
files=False,
list_files=False,
stream_processes=None,
):
'''
@ -233,7 +233,7 @@ def create_archive(
checkpoint_interval = storage_config.get('checkpoint_interval', None)
chunker_params = storage_config.get('chunker_params', None)
compression = storage_config.get('compression', None)
remote_rate_limit = storage_config.get('remote_rate_limit', None)
upload_rate_limit = storage_config.get('upload_rate_limit', None)
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
files_cache = location_config.get('files_cache')
@ -246,22 +246,22 @@ def create_archive(
atime_flags = ('--noatime',) if location_config.get('atime') is False else ()
if feature.available(feature.Feature.NOFLAGS, local_borg_version):
noflags_flags = ('--noflags',) if location_config.get('bsd_flags') is False else ()
noflags_flags = ('--noflags',) if location_config.get('flags') is False else ()
else:
noflags_flags = ('--nobsdflags',) if location_config.get('bsd_flags') is False else ()
noflags_flags = ('--nobsdflags',) if location_config.get('flags') is False else ()
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else ()
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_ids') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else ()
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else ()
if feature.available(feature.Feature.UPLOAD_RATELIMIT, local_borg_version):
upload_ratelimit_flags = (
('--upload-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
('--upload-ratelimit', str(upload_rate_limit)) if upload_rate_limit else ()
)
else:
upload_ratelimit_flags = (
('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
('--remote-ratelimit', str(upload_rate_limit)) if upload_rate_limit else ()
)
ensure_files_readable(location_config.get('patterns_from'), location_config.get('exclude_from'))
@ -290,7 +290,7 @@ def create_archive(
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--list', '--filter', 'AME-') if files and not json and not progress else ())
+ (('--list', '--filter', 'AME-') if list_files and not json and not progress else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--stats',) if stats and not json and not dry_run else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
@ -298,17 +298,13 @@ def create_archive(
+ (('--progress',) if progress else ())
+ (('--json',) if json else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (
'{repository}::{archive_name_format}'.format(
repository=repository, archive_name_format=archive_name_format
),
)
+ flags.make_repository_archive_flags(repository, archive_name_format, local_borg_version)
+ sources
)
if json:
output_log_level = None
elif (stats or files) and logger.getEffectiveLevel() == logging.WARNING:
elif (stats or list_files) and logger.getEffectiveLevel() == logging.WARNING:
output_log_level = logging.WARNING
else:
output_log_level = logging.INFO

View file

@ -1,7 +1,7 @@
import logging
import os
from borgmatic.borg import environment
from borgmatic.borg import environment, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@ -14,18 +14,19 @@ def export_tar_archive(
paths,
destination_path,
storage_config,
local_borg_version,
local_path='borg',
remote_path=None,
tar_filter=None,
files=False,
list_files=False,
strip_components=None,
):
'''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
export from the archive, a destination path to export to, a storage configuration dict, optional
local and remote Borg paths, an optional filter program, whether to include per-file details,
and an optional number of path components to strip, export the archive into the given
destination path as a tar-formatted file.
export from the archive, a destination path to export to, a storage configuration dict, the
local Borg version, optional local and remote Borg paths, an optional filter program, whether to
include per-file details, and an optional number of path components to strip, export the archive
into the given destination path as a tar-formatted file.
If the destination path is "-", then stream the output to stdout instead of to a file.
'''
@ -38,17 +39,21 @@ def export_tar_archive(
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--list',) if files else ())
+ (('--list',) if list_files else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--tar-filter', tar_filter) if tar_filter else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),)
+ flags.make_repository_archive_flags(
repository if ':' in repository else os.path.abspath(repository),
archive,
local_borg_version,
)
+ (destination_path,)
+ (tuple(paths) if paths else ())
)
if files and logger.getEffectiveLevel() == logging.WARNING:
if list_files and logger.getEffectiveLevel() == logging.WARNING:
output_log_level = logging.WARNING
else:
output_log_level = logging.INFO

View file

@ -2,14 +2,19 @@ import logging
import os
import subprocess
from borgmatic.borg import environment, feature
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def extract_last_archive_dry_run(
storage_config, repository, lock_wait=None, local_path='borg', remote_path=None
storage_config,
local_borg_version,
repository,
lock_wait=None,
local_path='borg',
remote_path=None,
):
'''
Perform an extraction dry-run of the most recent archive. If there are no archives, skip the
@ -23,40 +28,23 @@ def extract_last_archive_dry_run(
elif logger.isEnabledFor(logging.INFO):
verbosity_flags = ('--info',)
full_list_command = (
(local_path, 'list', '--short')
+ remote_path_flags
+ lock_wait_flags
+ verbosity_flags
+ (repository,)
)
borg_environment = environment.make_environment(storage_config)
list_output = execute_command(
full_list_command,
output_log_level=None,
borg_local_path=local_path,
extra_environment=borg_environment,
)
try:
last_archive_name = list_output.strip().splitlines()[-1]
except IndexError:
last_archive_name = rlist.resolve_archive_name(
repository, 'latest', storage_config, local_borg_version, local_path, remote_path
)
except ValueError:
logger.warning('No archives found. Skipping extract consistency check.')
return
list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
borg_environment = environment.make_environment(storage_config)
full_extract_command = (
(local_path, 'extract', '--dry-run')
+ remote_path_flags
+ lock_wait_flags
+ verbosity_flags
+ list_flag
+ (
'{repository}::{last_archive_name}'.format(
repository=repository, last_archive_name=last_archive_name
),
)
+ flags.make_repository_archive_flags(repository, last_archive_name, local_borg_version)
)
execute_command(
@ -95,9 +83,9 @@ def extract_archive(
raise ValueError('progress and extract_to_stdout cannot both be set')
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else ()
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_ids') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else ()
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_ids') else ()
full_command = (
(local_path, 'extract')
@ -111,7 +99,11 @@ def extract_archive(
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ (('--progress',) if progress else ())
+ (('--stdout',) if extract_to_stdout else ())
+ ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),)
+ flags.make_repository_archive_flags(
repository if ':' in repository else os.path.abspath(repository),
archive,
local_borg_version,
)
+ (tuple(paths) if paths else ())
)
@ -137,8 +129,8 @@ def extract_archive(
extra_environment=borg_environment,
)
# Don't give Borg local path, so as to error on warnings, as Borg only gives a warning if the
# restore paths don't exist in the archive!
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command(
full_command, working_directory=destination_path, extra_environment=borg_environment
)

View file

@ -9,6 +9,10 @@ class Feature(Enum):
NOFLAGS = 3
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
SEPARATE_REPOSITORY_ARCHIVE = 6
RCREATE = 7
RLIST = 8
RINFO = 9
FEATURE_TO_MINIMUM_BORG_VERSION = {
@ -17,6 +21,10 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.NOFLAGS: parse_version('1.2.0a8'), # borg create --noflags
Feature.NUMERIC_IDS: parse_version('1.2.0b3'), # borg create/extract/mount --numeric-ids
Feature.UPLOAD_RATELIMIT: parse_version('1.2.0b3'), # borg create --upload-ratelimit
Feature.SEPARATE_REPOSITORY_ARCHIVE: parse_version('2.0.0a2'), # --repo with separate archive
Feature.RCREATE: parse_version('2.0.0a2'), # borg rcreate
Feature.RLIST: parse_version('2.0.0a2'), # borg rlist
Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo
}

View file

@ -1,5 +1,7 @@
import itertools
from borgmatic.borg import feature
def make_flags(name, value):
'''
@ -29,3 +31,28 @@ def make_flags_from_arguments(arguments, excludes=()):
if name not in excludes and not name.startswith('_')
)
)
def make_repository_flags(repository, local_borg_version):
'''
Given the path of a Borg repository and the local Borg version, return Borg-version-appropriate
command-line flags (as a tuple) for selecting that repository.
'''
return (
('--repo',)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else ()
) + (repository,)
def make_repository_archive_flags(repository, archive, local_borg_version):
'''
Given the path of a Borg repository, an archive name or pattern, and the local Borg version,
return Borg-version-appropriate command-line flags (as a tuple) for selecting that repository
and archive.
'''
return (
('--repo', repository, archive)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (f'{repository}::{archive}',)
)

View file

@ -1,19 +1,23 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def display_archives_info(
repository, storage_config, info_arguments, local_path='borg', remote_path=None
repository,
storage_config,
local_borg_version,
info_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, and the arguments to the info
action, display summary information for Borg archives in the repository or return JSON summary
information.
Given a local or remote repository path, a storage config dict, the local Borg version, and the
arguments to the info action, display summary information for Borg archives in the repository or
return JSON summary information.
'''
lock_wait = storage_config.get('lock_wait', None)
@ -29,13 +33,25 @@ def display_archives_info(
if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json
else ()
)
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(info_arguments, excludes=('repository', 'archive'))
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ (
'::'.join((repository, info_arguments.archive))
if info_arguments.archive
else repository,
flags.make_flags('glob-archives', f'{info_arguments.prefix}*')
if info_arguments.prefix
else ()
)
+ flags.make_flags_from_arguments(
info_arguments, excludes=('repository', 'archive', 'prefix')
)
+ (
flags.make_repository_flags(repository, local_borg_version)
+ (
flags.make_flags('glob-archives', info_arguments.archive)
if feature.available(
feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version
)
else ()
)
)
)

View file

@ -1,62 +0,0 @@
import argparse
import logging
import subprocess
from borgmatic.borg import environment, info
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def initialize_repository(
repository,
storage_config,
encryption_mode,
append_only=None,
storage_quota=None,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage configuration dict, a Borg encryption mode,
whether the repository should be append-only, and the storage quota to use, initialize the
repository. If the repository already exists, then log and skip initialization.
'''
try:
info.display_archives_info(
repository,
storage_config,
argparse.Namespace(json=True, archive=None),
local_path,
remote_path,
)
logger.info('Repository already exists. Skipping initialization.')
return
except subprocess.CalledProcessError as error:
if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
raise
extra_borg_options = storage_config.get('extra_borg_options', {}).get('init', '')
init_command = (
(local_path, 'init')
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
)
# Do not capture output here, so as to support interactive prompts.
execute_command(
init_command,
output_file=DO_NOT_CAPTURE,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)

View file

@ -1,58 +1,31 @@
import argparse
import copy
import logging
import re
from borgmatic.borg import environment
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
from borgmatic.borg import environment, feature, flags, rlist
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None):
'''
Given a local or remote repository path, an archive name, a storage config dict, a local Borg
path, and a remote Borg path, simply return the archive name. But if the archive name is
"latest", then instead introspect the repository for the latest archive and return its name.
Raise ValueError if "latest" is given but there are no archives in the repository.
'''
if archive != "latest":
return archive
lock_wait = storage_config.get('lock_wait', None)
full_command = (
(local_path, 'list')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags('last', 1)
+ ('--short', repository)
)
output = execute_command(
full_command,
output_log_level=None,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)
try:
latest_archive = output.strip().splitlines()[-1]
except IndexError:
raise ValueError('No archives found in the repository')
logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
return latest_archive
MAKE_FLAGS_EXCLUDES = ('repository', 'archive', 'successful', 'paths', 'find_paths')
ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST = ('prefix', 'glob_archives', 'sort_by', 'first', 'last')
MAKE_FLAGS_EXCLUDES = (
'repository',
'archive',
'successful',
'paths',
'find_paths',
) + ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST
def make_list_command(
repository, storage_config, list_arguments, local_path='borg', remote_path=None
repository,
storage_config,
local_borg_version,
list_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, the arguments to the list
@ -73,13 +46,15 @@ def make_list_command(
if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
else ()
)
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES,)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ flags.make_flags_from_arguments(list_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ (
('::'.join((repository, list_arguments.archive)),)
flags.make_repository_archive_flags(
repository, list_arguments.archive, local_borg_version
)
if list_arguments.archive
else (repository,)
else flags.make_repository_flags(repository, local_borg_version)
)
+ (tuple(list_arguments.paths) if list_arguments.paths else ())
)
@ -109,29 +84,81 @@ def make_find_paths(find_paths):
)
def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
def list_archive(
repository,
storage_config,
local_borg_version,
list_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, the arguments to the list
action, and local and remote Borg paths, display the output of listing Borg archives in the
repository or return JSON output. Or, if an archive name is given, list the files in that
archive. Or, if list_arguments.find_paths are given, list the files by searching across multiple
archives.
Given a local or remote repository path, a storage config dict, the local Borg version, the
arguments to the list action, and local and remote Borg paths, display the output of listing
the files of a Borg archive (or return JSON output). If list_arguments.find_paths are given,
list the files by searching across multiple archives. If neither find_paths nor archive name
are given, instead list the archives in the given repository.
'''
if not list_arguments.archive and not list_arguments.find_paths:
if feature.available(feature.Feature.RLIST, local_borg_version):
logger.warning(
'Omitting the --archive flag on the list action is deprecated when using Borg 2.x+. Use the rlist action instead.'
)
rlist_arguments = argparse.Namespace(
repository=repository,
short=list_arguments.short,
format=list_arguments.format,
json=list_arguments.json,
prefix=list_arguments.prefix,
glob_archives=list_arguments.glob_archives,
sort_by=list_arguments.sort_by,
first=list_arguments.first,
last=list_arguments.last,
)
return rlist.list_repository(
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
)
if list_arguments.archive:
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST:
if getattr(list_arguments, name, None):
logger.warning(
f"The --{name.replace('_', '-')} flag on the list action is ignored when using the --archive flag."
)
if list_arguments.json:
raise ValueError(
'The --json flag on the list action is not supported when using the --archive/--find flags.'
)
borg_environment = environment.make_environment(storage_config)
# If there are any paths to find (and there's not a single archive already selected), start by
# getting a list of archives to search.
if list_arguments.find_paths and not list_arguments.archive:
repository_arguments = copy.copy(list_arguments)
repository_arguments.archive = None
repository_arguments.json = False
repository_arguments.format = None
rlist_arguments = argparse.Namespace(
repository=repository,
short=True,
format=None,
json=None,
prefix=list_arguments.prefix,
glob_archives=list_arguments.glob_archives,
sort_by=list_arguments.sort_by,
first=list_arguments.first,
last=list_arguments.last,
)
# Ask Borg to list archives. Capture its output for use below.
archive_lines = tuple(
execute_command(
make_list_command(
repository, storage_config, repository_arguments, local_path, remote_path
rlist.make_rlist_command(
repository,
storage_config,
local_borg_version,
rlist_arguments,
local_path,
remote_path,
),
output_log_level=None,
borg_local_path=local_path,
@ -144,27 +171,29 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
archive_lines = (list_arguments.archive,)
# For each archive listed by Borg, run list on the contents of that archive.
for archive_line in archive_lines:
try:
archive = archive_line.split()[0]
except (AttributeError, IndexError):
archive = None
if archive:
logger.warning(archive_line)
for archive in archive_lines:
logger.warning(f'{repository}: Listing archive {archive}')
archive_arguments = copy.copy(list_arguments)
archive_arguments.archive = archive
# This list call is to show the files in a single archive, not list multiple archives. So
# blank out any archive filtering flags. They'll break anyway in Borg 2.
for name in ARCHIVE_FILTER_FLAGS_MOVED_TO_RLIST:
setattr(archive_arguments, name, None)
main_command = make_list_command(
repository, storage_config, archive_arguments, local_path, remote_path
repository,
storage_config,
local_borg_version,
archive_arguments,
local_path,
remote_path,
) + make_find_paths(list_arguments.find_paths)
output = execute_command(
execute_command(
main_command,
output_log_level=None if list_arguments.json else logging.WARNING,
output_log_level=logging.WARNING,
borg_local_path=local_path,
extra_environment=borg_environment,
)
if list_arguments.json:
return output

View file

@ -1,6 +1,6 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@ -14,13 +14,15 @@ def mount_archive(
foreground,
options,
storage_config,
local_borg_version,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, an optional archive name, a filesystem mount point,
zero or more paths to mount from the archive, extra Borg mount options, a storage configuration
dict, and optional local and remote Borg paths, mount the archive onto the mount point.
dict, the local Borg version, and optional local and remote Borg paths, mount the archive onto
the mount point.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
@ -34,7 +36,18 @@ def mount_archive(
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--foreground',) if foreground else ())
+ (('-o', options) if options else ())
+ (('::'.join((repository, archive)),) if archive else (repository,))
+ (
(
flags.make_repository_flags(repository, local_borg_version)
+ ('--glob-archives', archive)
)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (
flags.make_repository_archive_flags(repository, archive, local_borg_version)
if archive
else flags.make_repository_flags(repository, local_borg_version)
)
)
+ (mount_point,)
+ (tuple(paths) if paths else ())
)

View file

@ -1,12 +1,12 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def _make_prune_flags(retention_config):
def make_prune_flags(retention_config):
'''
Given a retention config dict mapping from option name to value, tranform it into an iterable of
command-line name-value flag pairs.
@ -23,11 +23,9 @@ def _make_prune_flags(retention_config):
)
'''
config = retention_config.copy()
if 'prefix' not in config:
config['prefix'] = '{hostname}-'
elif not config['prefix']:
config.pop('prefix')
prefix = config.pop('prefix', '{hostname}-')
if prefix:
config['glob_archives'] = f'{prefix}*'
return (
('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items()
@ -39,10 +37,11 @@ def prune_archives(
repository,
storage_config,
retention_config,
local_borg_version,
local_path='borg',
remote_path=None,
stats=False,
files=False,
list_archives=False,
):
'''
Given dry-run flag, a local or remote repository path, a storage config dict, and a
@ -55,20 +54,20 @@ def prune_archives(
full_command = (
(local_path, 'prune')
+ tuple(element for pair in _make_prune_flags(retention_config) for element in pair)
+ tuple(element for pair in make_prune_flags(retention_config) for element in pair)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--stats',) if stats and not dry_run else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--list',) if files else ())
+ (('--list',) if list_archives else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
+ flags.make_repository_flags(repository, local_borg_version)
)
if (stats or files) and logger.getEffectiveLevel() == logging.WARNING:
if (stats or list_archives) and logger.getEffectiveLevel() == logging.WARNING:
output_log_level = logging.WARNING
else:
output_log_level = logging.INFO

81
borgmatic/borg/rcreate.py Normal file
View file

@ -0,0 +1,81 @@
import argparse
import logging
import subprocess
from borgmatic.borg import environment, feature, flags, rinfo
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def create_repository(
dry_run,
repository,
storage_config,
local_borg_version,
encryption_mode,
source_repository=None,
copy_crypt_key=False,
append_only=None,
storage_quota=None,
make_parent_dirs=False,
local_path='borg',
remote_path=None,
):
'''
Given a dry-run flag, a local or remote repository path, a storage configuration dict, the local
Borg version, a Borg encryption mode, the path to another repo whose key material should be
reused, whether the repository should be append-only, and the storage quota to use, create the
repository. If the repository already exists, then log and skip creation.
'''
try:
rinfo.display_repository_info(
repository,
storage_config,
local_borg_version,
argparse.Namespace(json=True),
local_path,
remote_path,
)
logger.info(f'{repository}: Repository already exists. Skipping creation.')
return
except subprocess.CalledProcessError as error:
if error.returncode != RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
raise
extra_borg_options = storage_config.get('extra_borg_options', {}).get('rcreate', '')
rcreate_command = (
(local_path,)
+ (
('rcreate',)
if feature.available(feature.Feature.RCREATE, local_borg_version)
else ('init',)
)
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--other-repo', source_repository) if source_repository else ())
+ (('--copy-crypt-key',) if copy_crypt_key else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
+ (('--make-parent-dirs',) if make_parent_dirs else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ flags.make_repository_flags(repository, local_borg_version)
)
if dry_run:
logging.info(f'{repository}: Skipping repository creation (dry run)')
return
# Do not capture output here, so as to support interactive prompts.
execute_command(
rcreate_command,
output_file=DO_NOT_CAPTURE,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)

52
borgmatic/borg/rinfo.py Normal file
View file

@ -0,0 +1,52 @@
import logging
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def display_repository_info(
repository,
storage_config,
local_borg_version,
rinfo_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, the local Borg version, and the
arguments to the rinfo action, display summary information for the Borg repository or return
JSON summary information.
'''
lock_wait = storage_config.get('lock_wait', None)
full_command = (
(local_path,)
+ (
('rinfo',)
if feature.available(feature.Feature.RINFO, local_borg_version)
else ('info',)
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rinfo_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rinfo_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ (('--json',) if rinfo_arguments.json else ())
+ flags.make_repository_flags(repository, local_borg_version)
)
return execute_command(
full_command,
output_log_level=None if rinfo_arguments.json else logging.WARNING,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)

126
borgmatic/borg/rlist.py Normal file
View file

@ -0,0 +1,126 @@
import logging
from borgmatic.borg import environment, feature, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def resolve_archive_name(
repository, archive, storage_config, local_borg_version, local_path='borg', remote_path=None
):
'''
Given a local or remote repository path, an archive name, a storage config dict, a local Borg
path, and a remote Borg path, simply return the archive name. But if the archive name is
"latest", then instead introspect the repository for the latest archive and return its name.
Raise ValueError if "latest" is given but there are no archives in the repository.
'''
if archive != "latest":
return archive
lock_wait = storage_config.get('lock_wait', None)
full_command = (
(
local_path,
'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
)
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ flags.make_flags('last', 1)
+ ('--short',)
+ flags.make_repository_flags(repository, local_borg_version)
)
output = execute_command(
full_command,
output_log_level=None,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)
try:
latest_archive = output.strip().splitlines()[-1]
except IndexError:
raise ValueError('No archives found in the repository')
logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
return latest_archive
MAKE_FLAGS_EXCLUDES = ('repository', 'prefix')
def make_rlist_command(
repository,
storage_config,
local_borg_version,
rlist_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, the local Borg version, the
arguments to the rlist action, and local and remote Borg paths, return a command as a tuple to
list archives with a repository.
'''
lock_wait = storage_config.get('lock_wait', None)
return (
(
local_path,
'rlist' if feature.available(feature.Feature.RLIST, local_borg_version) else 'list',
)
+ (
('--info',)
if logger.getEffectiveLevel() == logging.INFO and not rlist_arguments.json
else ()
)
+ (
('--debug', '--show-rc')
if logger.isEnabledFor(logging.DEBUG) and not rlist_arguments.json
else ()
)
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', lock_wait)
+ (
flags.make_flags('glob-archives', f'{rlist_arguments.prefix}*')
if rlist_arguments.prefix
else ()
)
+ flags.make_flags_from_arguments(rlist_arguments, excludes=MAKE_FLAGS_EXCLUDES)
+ flags.make_repository_flags(repository, local_borg_version)
)
def list_repository(
repository,
storage_config,
local_borg_version,
rlist_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, the local Borg version, the
arguments to the list action, and local and remote Borg paths, display the output of listing
Borg archives in the given repository (or return JSON output).
'''
borg_environment = environment.make_environment(storage_config)
main_command = make_rlist_command(
repository, storage_config, local_borg_version, rlist_arguments, local_path, remote_path
)
output = execute_command(
main_command,
output_log_level=None if rlist_arguments.json else logging.WARNING,
borg_local_path=local_path,
extra_environment=borg_environment,
)
if rlist_arguments.json:
return output

View file

@ -0,0 +1,45 @@
import logging
from borgmatic.borg import environment, flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def transfer_archives(
dry_run,
repository,
storage_config,
local_borg_version,
transfer_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a dry-run flag, a local or remote repository path, a storage config dict, the local Borg
version, and the arguments to the transfer action, transfer archives to the given repository.
'''
full_command = (
(local_path, 'transfer')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ flags.make_flags('remote-path', remote_path)
+ flags.make_flags('lock-wait', storage_config.get('lock_wait', None))
+ flags.make_flags(
'glob-archives', transfer_arguments.glob_archives or transfer_arguments.archive
)
+ flags.make_flags_from_arguments(
transfer_arguments,
excludes=('repository', 'source_repository', 'archive', 'glob_archives'),
)
+ flags.make_repository_flags(repository, local_borg_version)
+ flags.make_flags('other-repo', transfer_arguments.source_repository)
+ flags.make_flags('dry-run', dry_run)
)
return execute_command(
full_command,
output_log_level=logging.WARNING,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),
)

View file

@ -4,18 +4,21 @@ from argparse import Action, ArgumentParser
from borgmatic.config import collect
SUBPARSER_ALIASES = {
'init': ['--init', '-I'],
'prune': ['--prune', '-p'],
'rcreate': ['init', '-I'],
'prune': ['-p'],
'compact': [],
'create': ['--create', '-C'],
'check': ['--check', '-k'],
'extract': ['--extract', '-x'],
'export-tar': ['--export-tar'],
'mount': ['--mount', '-m'],
'umount': ['--umount', '-u'],
'restore': ['--restore', '-r'],
'list': ['--list', '-l'],
'info': ['--info', '-i'],
'create': ['-C'],
'check': ['-k'],
'extract': ['-x'],
'export-tar': [],
'mount': ['-m'],
'umount': ['-u'],
'restore': ['-r'],
'rlist': [],
'list': ['-l'],
'rinfo': [],
'info': ['-i'],
'transfer': [],
'borg': [],
}
@ -222,33 +225,92 @@ def make_parsers():
metavar='',
help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:',
)
init_parser = subparsers.add_parser(
'init',
aliases=SUBPARSER_ALIASES['init'],
help='Initialize an empty Borg repository',
description='Initialize an empty Borg repository',
rcreate_parser = subparsers.add_parser(
'rcreate',
aliases=SUBPARSER_ALIASES['rcreate'],
help='Create a new, empty Borg repository',
description='Create a new, empty Borg repository',
add_help=False,
)
init_group = init_parser.add_argument_group('init arguments')
init_group.add_argument(
rcreate_group = rcreate_parser.add_argument_group('rcreate arguments')
rcreate_group.add_argument(
'-e',
'--encryption',
dest='encryption_mode',
help='Borg repository encryption mode',
required=True,
)
init_group.add_argument(
'--append-only',
dest='append_only',
rcreate_group.add_argument(
'--source-repository',
'--other-repo',
metavar='KEY_REPOSITORY',
help='Path to an existing Borg repository whose key material should be reused (Borg 2.x+ only)',
)
rcreate_group.add_argument(
'--copy-crypt-key',
action='store_true',
help='Create an append-only repository',
help='Copy the crypt key used for authenticated encryption from the source repository, defaults to a new random key (Borg 2.x+ only)',
)
init_group.add_argument(
'--storage-quota',
dest='storage_quota',
help='Create a repository with a fixed storage quota',
rcreate_group.add_argument(
'--append-only', action='store_true', help='Create an append-only repository',
)
rcreate_group.add_argument(
'--storage-quota', help='Create a repository with a fixed storage quota',
)
rcreate_group.add_argument(
'--make-parent-dirs',
action='store_true',
help='Create any missing parent directories of the repository directory',
)
rcreate_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
transfer_parser = subparsers.add_parser(
'transfer',
aliases=SUBPARSER_ALIASES['transfer'],
help='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)',
description='Transfer archives from one repository to another, optionally upgrading the transferred data (Borg 2.0+ only)',
add_help=False,
)
transfer_group = transfer_parser.add_argument_group('transfer arguments')
transfer_group.add_argument(
'--repository',
help='Path of existing destination repository to transfer archives to, defaults to the configured repository if there is only one',
)
transfer_group.add_argument(
'--source-repository',
help='Path of existing source repository to transfer archives from',
required=True,
)
transfer_group.add_argument(
'--archive',
help='Name of single archive to transfer (or "latest"), defaults to transferring all archives',
)
transfer_group.add_argument(
'--upgrader',
help='Upgrader type used to convert the transfered data, e.g. "From12To20" to upgrade data from Borg 1.2 to 2.0 format, defaults to no conversion',
)
transfer_group.add_argument(
'-a',
'--glob-archives',
metavar='GLOB',
help='Only transfer archives with names matching this glob',
)
transfer_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
)
transfer_group.add_argument(
'--first',
metavar='N',
help='Only transfer first N archives after other filters are applied',
)
transfer_group.add_argument(
'--last', metavar='N', help='Only transfer last N archives after other filters are applied'
)
transfer_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
prune_parser = subparsers.add_parser(
'prune',
@ -266,7 +328,7 @@ def make_parsers():
help='Display statistics of archive',
)
prune_group.add_argument(
'--files', dest='files', default=False, action='store_true', help='Show per-file details'
'--list', dest='list_archives', action='store_true', help='List archives kept/pruned'
)
prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
@ -290,7 +352,7 @@ def make_parsers():
dest='cleanup_commits',
default=False,
action='store_true',
help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1',
help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1 (flag in Borg 1.2 only)',
)
compact_group.add_argument(
'--threshold',
@ -305,8 +367,8 @@ def make_parsers():
create_parser = subparsers.add_parser(
'create',
aliases=SUBPARSER_ALIASES['create'],
help='Create archives (actually perform backups)',
description='Create archives (actually perform backups)',
help='Create an archive (actually perform a backup)',
description='Create an archive (actually perform a backup)',
add_help=False,
)
create_group = create_parser.add_argument_group('create arguments')
@ -325,7 +387,7 @@ def make_parsers():
help='Display statistics of archive',
)
create_group.add_argument(
'--files', dest='files', default=False, action='store_true', help='Show per-file details'
'--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
)
create_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
@ -443,14 +505,14 @@ def make_parsers():
'--destination',
metavar='PATH',
dest='destination',
help='Path to destination export tar file, or "-" for stdout (but be careful about dirtying output with --verbosity or --files)',
help='Path to destination export tar file, or "-" for stdout (but be careful about dirtying output with --verbosity or --list)',
required=True,
)
export_tar_group.add_argument(
'--tar-filter', help='Name of filter program to pipe data through'
)
export_tar_group.add_argument(
'--files', default=False, action='store_true', help='Show per-file details'
'--list', '--files', dest='list_files', action='store_true', help='Show per-file details'
)
export_tar_group.add_argument(
'--strip-components',
@ -543,18 +605,54 @@ def make_parsers():
'-h', '--help', action='help', help='Show this help message and exit'
)
rlist_parser = subparsers.add_parser(
'rlist',
aliases=SUBPARSER_ALIASES['rlist'],
help='List repository',
description='List the archives in a repository',
add_help=False,
)
rlist_group = rlist_parser.add_argument_group('rlist arguments')
rlist_group.add_argument(
'--repository', help='Path of repository to list, defaults to the configured repositories',
)
rlist_group.add_argument(
'--short', default=False, action='store_true', help='Output only archive names'
)
rlist_group.add_argument('--format', help='Format for archive listing')
rlist_group.add_argument(
'--json', default=False, action='store_true', help='Output results as JSON'
)
rlist_group.add_argument(
'-P', '--prefix', help='Only list archive names starting with this prefix'
)
rlist_group.add_argument(
'-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
)
rlist_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
)
rlist_group.add_argument(
'--first', metavar='N', help='List first N archives after other filters are applied'
)
rlist_group.add_argument(
'--last', metavar='N', help='List last N archives after other filters are applied'
)
rlist_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
list_parser = subparsers.add_parser(
'list',
aliases=SUBPARSER_ALIASES['list'],
help='List archives',
description='List archives or the contents of an archive',
help='List archive',
description='List the files in an archive or search for a file across archives',
add_help=False,
)
list_group = list_parser.add_argument_group('list arguments')
list_group.add_argument(
'--repository', help='Path of repository to list, defaults to the configured repositories',
'--repository',
help='Path of repository containing archive to list, defaults to the configured repositories',
)
list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
list_group.add_argument('--archive', help='Name of the archive to list (or "latest")')
list_group.add_argument(
'--path',
metavar='PATH',
@ -570,7 +668,7 @@ def make_parsers():
help='Partial paths or patterns to search for and list across multiple archives',
)
list_group.add_argument(
'--short', default=False, action='store_true', help='Output only archive or path names'
'--short', default=False, action='store_true', help='Output only path names'
)
list_group.add_argument('--format', help='Format for file listing')
list_group.add_argument(
@ -586,7 +684,7 @@ def make_parsers():
'--successful',
default=True,
action='store_true',
help='Deprecated in favor of listing successful (non-checkpoint) backups by default in newer versions of Borg',
help='Deprecated; no effect. Newer versions of Borg shows successful (non-checkpoint) archives by default.',
)
list_group.add_argument(
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
@ -611,17 +709,34 @@ def make_parsers():
)
list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
rinfo_parser = subparsers.add_parser(
'rinfo',
aliases=SUBPARSER_ALIASES['rinfo'],
help='Show repository summary information such as disk space used',
description='Show repository summary information such as disk space used',
add_help=False,
)
rinfo_group = rinfo_parser.add_argument_group('rinfo arguments')
rinfo_group.add_argument(
'--repository',
help='Path of repository to show info for, defaults to the configured repository if there is only one',
)
rinfo_group.add_argument(
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
)
rinfo_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
info_parser = subparsers.add_parser(
'info',
aliases=SUBPARSER_ALIASES['info'],
help='Display summary information on archives',
description='Display summary information on archives',
help='Show archive summary information such as disk space used',
description='Show archive summary information such as disk space used',
add_help=False,
)
info_group = info_parser.add_argument_group('info arguments')
info_group.add_argument(
'--repository',
help='Path of repository to show info for, defaults to the configured repository if there is only one',
help='Path of repository containing archive to show info for, defaults to the configured repository if there is only one',
)
info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
info_group.add_argument(
@ -688,18 +803,32 @@ def parse_arguments(*unparsed_arguments):
if arguments['global'].excludes_filename:
raise ValueError(
'The --excludes option has been replaced with exclude_patterns in configuration'
'The --excludes flag has been replaced with exclude_patterns in configuration.'
)
if 'init' in arguments and arguments['global'].dry_run:
raise ValueError('The init action cannot be used with the --dry-run option')
if (
('list' in arguments and 'rinfo' in arguments and arguments['list'].json)
or ('list' in arguments and 'info' in arguments and arguments['list'].json)
or ('rinfo' in arguments and 'info' in arguments and arguments['rinfo'].json)
):
raise ValueError('With the --json flag, multiple actions cannot be used together.')
if (
'list' in arguments
and 'info' in arguments
and arguments['list'].json
and arguments['info'].json
'transfer' in arguments
and arguments['transfer'].archive
and arguments['transfer'].glob_archives
):
raise ValueError('With the --json option, list and info actions cannot be used together')
raise ValueError(
'With the transfer action, only one of --archive and --glob-archives flags can be used.'
)
if 'info' in arguments and (
(arguments['info'].archive and arguments['info'].prefix)
or (arguments['info'].archive and arguments['info'].glob_archives)
or (arguments['info'].prefix and arguments['info'].glob_archives)
):
raise ValueError(
'With the info action, only one of --archive, --prefix, or --glob-archives flags can be used.'
)
return arguments

View file

@ -20,10 +20,13 @@ from borgmatic.borg import export_tar as borg_export_tar
from borgmatic.borg import extract as borg_extract
from borgmatic.borg import feature as borg_feature
from borgmatic.borg import info as borg_info
from borgmatic.borg import init as borg_init
from borgmatic.borg import list as borg_list
from borgmatic.borg import mount as borg_mount
from borgmatic.borg import prune as borg_prune
from borgmatic.borg import rcreate as borg_rcreate
from borgmatic.borg import rinfo as borg_rinfo
from borgmatic.borg import rlist as borg_rlist
from borgmatic.borg import transfer as borg_transfer
from borgmatic.borg import umount as borg_umount
from borgmatic.borg import version as borg_version
from borgmatic.commands.arguments import parse_arguments
@ -249,14 +252,39 @@ def run_actions(
'repositories': ','.join(location['repositories']),
}
if 'init' in arguments:
logger.info('{}: Initializing repository'.format(repository))
borg_init.initialize_repository(
command.execute_hook(
hooks.get('before_actions'),
hooks.get('umask'),
config_filename,
'pre-actions',
global_arguments.dry_run,
**hook_context,
)
if 'rcreate' in arguments:
logger.info('{}: Creating repository'.format(repository))
borg_rcreate.create_repository(
global_arguments.dry_run,
repository,
storage,
arguments['init'].encryption_mode,
arguments['init'].append_only,
arguments['init'].storage_quota,
local_borg_version,
arguments['rcreate'].encryption_mode,
arguments['rcreate'].source_repository,
arguments['rcreate'].copy_crypt_key,
arguments['rcreate'].append_only,
arguments['rcreate'].storage_quota,
arguments['rcreate'].make_parent_dirs,
local_path=local_path,
remote_path=remote_path,
)
if 'transfer' in arguments:
logger.info(f'{repository}: Transferring archives to repository')
borg_transfer.transfer_archives(
global_arguments.dry_run,
repository,
storage,
local_borg_version,
transfer_arguments=arguments['transfer'],
local_path=local_path,
remote_path=remote_path,
)
@ -275,10 +303,11 @@ def run_actions(
repository,
storage,
retention,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
stats=arguments['prune'].stats,
files=arguments['prune'].files,
list_archives=arguments['prune'].list_archives,
)
command.execute_hook(
hooks.get('after_prune'),
@ -302,6 +331,7 @@ def run_actions(
global_arguments.dry_run,
repository,
storage,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
progress=arguments['compact'].progress,
@ -358,7 +388,7 @@ def run_actions(
progress=arguments['create'].progress,
stats=arguments['create'].stats,
json=arguments['create'].json,
files=arguments['create'].files,
list_files=arguments['create'].list_files,
stream_processes=stream_processes,
)
if json_output: # pragma: nocover
@ -396,6 +426,7 @@ def run_actions(
location,
storage,
consistency,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
progress=arguments['check'].progress,
@ -429,8 +460,13 @@ def run_actions(
borg_extract.extract_archive(
global_arguments.dry_run,
repository,
borg_list.resolve_archive_name(
repository, arguments['extract'].archive, storage, local_path, remote_path
borg_rlist.resolve_archive_name(
repository,
arguments['extract'].archive,
storage,
local_borg_version,
local_path,
remote_path,
),
arguments['extract'].paths,
location,
@ -462,16 +498,22 @@ def run_actions(
borg_export_tar.export_tar_archive(
global_arguments.dry_run,
repository,
borg_list.resolve_archive_name(
repository, arguments['export-tar'].archive, storage, local_path, remote_path
borg_rlist.resolve_archive_name(
repository,
arguments['export-tar'].archive,
storage,
local_borg_version,
local_path,
remote_path,
),
arguments['export-tar'].paths,
arguments['export-tar'].destination,
storage,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
tar_filter=arguments['export-tar'].tar_filter,
files=arguments['export-tar'].files,
list_files=arguments['export-tar'].list_files,
strip_components=arguments['export-tar'].strip_components,
)
if 'mount' in arguments:
@ -487,14 +529,20 @@ def run_actions(
borg_mount.mount_archive(
repository,
borg_list.resolve_archive_name(
repository, arguments['mount'].archive, storage, local_path, remote_path
borg_rlist.resolve_archive_name(
repository,
arguments['mount'].archive,
storage,
local_borg_version,
local_path,
remote_path,
),
arguments['mount'].mount_point,
arguments['mount'].paths,
arguments['mount'].foreground,
arguments['mount'].options,
storage,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
)
@ -520,8 +568,13 @@ def run_actions(
if 'all' in restore_names:
restore_names = []
archive_name = borg_list.resolve_archive_name(
repository, arguments['restore'].archive, storage, local_path, remote_path
archive_name = borg_rlist.resolve_archive_name(
repository,
arguments['restore'].archive,
storage,
local_borg_version,
local_path,
remote_path,
)
found_names = set()
@ -591,39 +644,87 @@ def run_actions(
', '.join(missing_names)
)
)
if 'rlist' in arguments:
if arguments['rlist'].repository is None or validate.repositories_match(
repository, arguments['rlist'].repository
):
rlist_arguments = copy.copy(arguments['rlist'])
if not rlist_arguments.json: # pragma: nocover
logger.warning('{}: Listing repository'.format(repository))
json_output = borg_rlist.list_repository(
repository,
storage,
local_borg_version,
rlist_arguments=rlist_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if 'list' in arguments:
if arguments['list'].repository is None or validate.repositories_match(
repository, arguments['list'].repository
):
list_arguments = copy.copy(arguments['list'])
if not list_arguments.json: # pragma: nocover
logger.warning('{}: Listing archives'.format(repository))
list_arguments.archive = borg_list.resolve_archive_name(
repository, list_arguments.archive, storage, local_path, remote_path
if list_arguments.find_paths:
logger.warning('{}: Searching archives'.format(repository))
else:
logger.warning('{}: Listing archive'.format(repository))
list_arguments.archive = borg_rlist.resolve_archive_name(
repository,
list_arguments.archive,
storage,
local_borg_version,
local_path,
remote_path,
)
json_output = borg_list.list_archives(
json_output = borg_list.list_archive(
repository,
storage,
local_borg_version,
list_arguments=list_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if 'rinfo' in arguments:
if arguments['rinfo'].repository is None or validate.repositories_match(
repository, arguments['rinfo'].repository
):
rinfo_arguments = copy.copy(arguments['rinfo'])
if not rinfo_arguments.json: # pragma: nocover
logger.warning('{}: Displaying repository summary information'.format(repository))
json_output = borg_rinfo.display_repository_info(
repository,
storage,
local_borg_version,
rinfo_arguments=rinfo_arguments,
local_path=local_path,
remote_path=remote_path,
)
if json_output: # pragma: nocover
yield json.loads(json_output)
if 'info' in arguments:
if arguments['info'].repository is None or validate.repositories_match(
repository, arguments['info'].repository
):
info_arguments = copy.copy(arguments['info'])
if not info_arguments.json: # pragma: nocover
logger.warning('{}: Displaying summary info for archives'.format(repository))
info_arguments.archive = borg_list.resolve_archive_name(
repository, info_arguments.archive, storage, local_path, remote_path
logger.warning('{}: Displaying archive summary information'.format(repository))
info_arguments.archive = borg_rlist.resolve_archive_name(
repository,
info_arguments.archive,
storage,
local_borg_version,
local_path,
remote_path,
)
json_output = borg_info.display_archives_info(
repository,
storage,
local_borg_version,
info_arguments=info_arguments,
local_path=local_path,
remote_path=remote_path,
@ -635,18 +736,33 @@ def run_actions(
repository, arguments['borg'].repository
):
logger.warning('{}: Running arbitrary Borg command'.format(repository))
archive_name = borg_list.resolve_archive_name(
repository, arguments['borg'].archive, storage, local_path, remote_path
archive_name = borg_rlist.resolve_archive_name(
repository,
arguments['borg'].archive,
storage,
local_borg_version,
local_path,
remote_path,
)
borg_borg.run_arbitrary_borg(
repository,
storage,
local_borg_version,
options=arguments['borg'].options,
archive=archive_name,
local_path=local_path,
remote_path=remote_path,
)
command.execute_hook(
hooks.get('after_actions'),
hooks.get('umask'),
config_filename,
'post-actions',
global_arguments.dry_run,
**hook_context,
)
def load_configurations(config_filenames, overrides=None, resolve_env=True):
'''
@ -661,9 +777,10 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
# Parse and load each configuration file.
for config_filename in config_filenames:
try:
configs[config_filename] = validate.parse_configuration(
configs[config_filename], parse_logs = validate.parse_configuration(
config_filename, validate.schema_filename(), overrides, resolve_env
)
logs.extend(parse_logs)
except PermissionError:
logs.extend(
[
@ -768,21 +885,21 @@ def collect_configuration_run_summary_logs(configs, arguments):
any, to stdout.
'''
# Run cross-file validation checks.
if 'extract' in arguments:
repository = arguments['extract'].repository
elif 'list' in arguments and arguments['list'].archive:
repository = arguments['list'].repository
elif 'mount' in arguments:
repository = arguments['mount'].repository
else:
repository = None
repository = None
if repository:
try:
validate.guard_configuration_contains_repository(repository, configs)
except ValueError as error:
yield from log_error_records(str(error))
return
for action_name, action_arguments in arguments.items():
if hasattr(action_arguments, 'repository'):
repository = getattr(action_arguments, 'repository')
break
try:
if 'extract' in arguments or 'mount' in arguments:
validate.guard_single_repository_selected(repository, configs)
validate.guard_configuration_contains_repository(repository, configs)
except ValueError as error:
yield from log_error_records(str(error))
return
if not configs:
yield from log_error_records(

View file

@ -283,7 +283,7 @@ def generate_sample_configuration(
if source_filename:
source_config = load.load_configuration(source_filename)
normalize.normalize(source_config)
normalize.normalize(source_filename, source_config)
destination_config = merge_source_configuration_into_destination(
_schema_to_sample_configuration(schema), source_config

View file

@ -1,8 +1,14 @@
def normalize(config):
import logging
def normalize(config_filename, config):
'''
Given a configuration dict, apply particular hard-coded rules to normalize its contents to
adhere to the configuration schema.
Given a configuration filename and a configuration dict of its loaded contents, apply particular
hard-coded rules to normalize the configuration to adhere to the current schema. Return any log
message warnings produced based on the normalization performed.
'''
logs = []
# Upgrade exclude_if_present from a string to a list.
exclude_if_present = config.get('location', {}).get('exclude_if_present')
if isinstance(exclude_if_present, str):
@ -29,3 +35,50 @@ def normalize(config):
checks = config.get('consistency', {}).get('checks')
if isinstance(checks, list) and len(checks) and isinstance(checks[0], str):
config['consistency']['checks'] = [{'name': check_type} for check_type in checks]
# Rename various configuration options.
numeric_owner = config.get('location', {}).pop('numeric_owner', None)
if numeric_owner is not None:
config['location']['numeric_ids'] = numeric_owner
bsd_flags = config.get('location', {}).pop('bsd_flags', None)
if bsd_flags is not None:
config['location']['flags'] = bsd_flags
remote_rate_limit = config.get('storage', {}).pop('remote_rate_limit', None)
if remote_rate_limit is not None:
config['storage']['upload_rate_limit'] = remote_rate_limit
# Upgrade remote repositories to ssh:// syntax, required in Borg 2.
repositories = config.get('location', {}).get('repositories')
if repositories:
config['location']['repositories'] = []
for repository in repositories:
if '~' in repository:
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Repository paths containing "~" are deprecated in borgmatic and no longer work in Borg 2.x+.',
)
)
)
if ':' in repository and not repository.startswith('ssh://'):
rewritten_repository = (
f"ssh://{repository.replace(':~', '/~').replace(':/', '/').replace(':', '/./')}"
)
logs.append(
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg=f'{config_filename}: Remote repository paths without ssh:// syntax are deprecated. Interpreting "{repository}" as "{rewritten_repository}"',
)
)
)
config['location']['repositories'].append(rewritten_repository)
else:
config['location']['repositories'].append(repository)
return logs

View file

@ -58,7 +58,7 @@ properties:
database hook is used, the setting here is ignored and
one_file_system is considered true.
example: true
numeric_owner:
numeric_ids:
type: boolean
description: |
Only store/extract numeric user and group identifiers.
@ -90,10 +90,10 @@ properties:
used, the setting here is ignored and read_special is
considered true.
example: false
bsd_flags:
flags:
type: boolean
description: |
Record bsdflags (e.g. NODUMP, IMMUTABLE) in archive.
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
Defaults to true.
example: true
files_cache:
@ -145,10 +145,10 @@ properties:
type: string
description: |
Any paths matching these patterns are excluded from backups.
Globs and tildes are expanded. (Note however that a glob
pattern must either start with a glob or be an absolute
path.) Do not backslash spaces in path names. See the output
of "borg help patterns" for more details.
Globs and tildes are expanded. Note that a glob pattern must
either start with a glob or be an absolute path. Do not
backslash spaces in path names. See the output of "borg help
patterns" for more details.
example:
- '*.pyc'
- /home/*/.cache
@ -255,7 +255,7 @@ properties:
http://borgbackup.readthedocs.io/en/stable/usage/create.html
for details. Defaults to "lz4".
example: lz4
remote_rate_limit:
upload_rate_limit:
type: integer
description: |
Remote network upload rate limit in kiBytes/second. Defaults
@ -538,13 +538,22 @@ properties:
prevent potential shell injection or privilege escalation.
additionalProperties: false
properties:
before_actions:
type: array
items:
type: string
description: |
List of one or more shell commands or scripts to execute
before all the actions for each repository.
example:
- echo "Starting actions."
before_backup:
type: array
items:
type: string
description: |
List of one or more shell commands or scripts to execute
before creating a backup, run once per configuration file.
before creating a backup, run once per repository.
example:
- echo "Starting a backup."
before_prune:
@ -553,7 +562,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
before pruning, run once per configuration file.
before pruning, run once per repository.
example:
- echo "Starting pruning."
before_compact:
@ -562,7 +571,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
before compaction, run once per configuration file.
before compaction, run once per repository.
example:
- echo "Starting compaction."
before_check:
@ -571,7 +580,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
before consistency checks, run once per configuration file.
before consistency checks, run once per repository.
example:
- echo "Starting checks."
before_extract:
@ -580,7 +589,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
before extracting a backup, run once per configuration file.
before extracting a backup, run once per repository.
example:
- echo "Starting extracting."
after_backup:
@ -589,7 +598,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
after creating a backup, run once per configuration file.
after creating a backup, run once per repository.
example:
- echo "Finished a backup."
after_compact:
@ -598,7 +607,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
after compaction, run once per configuration file.
after compaction, run once per repository.
example:
- echo "Finished compaction."
after_prune:
@ -607,7 +616,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
after pruning, run once per configuration file.
after pruning, run once per repository.
example:
- echo "Finished pruning."
after_check:
@ -616,7 +625,7 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
after consistency checks, run once per configuration file.
after consistency checks, run once per repository.
example:
- echo "Finished checks."
after_extract:
@ -625,9 +634,18 @@ properties:
type: string
description: |
List of one or more shell commands or scripts to execute
after extracting a backup, run once per configuration file.
after extracting a backup, run once per repository.
example:
- echo "Finished extracting."
after_actions:
type: array
items:
type: string
description: |
List of one or more shell commands or scripts to execute
after all actions for each repository.
example:
- echo "Finished actions."
on_error:
type: array
items:
@ -1012,6 +1030,12 @@ properties:
Healthchecks ping URL or UUID to notify when a
backup begins, ends, or errors.
example: https://hc-ping.com/your-uuid-here
verify_tls:
type: boolean
description: |
Verify the TLS certificate of the ping URL host.
Defaults to true.
example: false
send_logs:
type: boolean
description: |

View file

@ -89,6 +89,9 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
{'location': {'source_directories': ['/home', '/etc'], 'repository': 'hostname.borg'},
'retention': {'keep_daily': 7}, 'consistency': {'checks': ['repository', 'archives']}}
Also return a sequence of logging.LogRecord instances containing any warnings about the
configuration.
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
have permissions to read the file, or Validation_error if the config does not match the schema.
'''
@ -98,8 +101,8 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
raise Validation_error(config_filename, (str(error),))
normalize.normalize(config)
override.apply_overrides(config, overrides)
logs = normalize.normalize(config_filename, config)
if resolve_env:
environment.resolve_env_variables(config)
@ -116,7 +119,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
apply_logical_validation(config_filename, config)
return config
return config, logs
def normalize_repository_path(repository):
@ -140,27 +143,13 @@ def repositories_match(first, second):
def guard_configuration_contains_repository(repository, configurations):
'''
Given a repository path and a dict mapping from config filename to corresponding parsed config
dict, ensure that the repository is declared exactly once in all of the configurations.
If no repository is given, then error if there are multiple configured repositories.
dict, ensure that the repository is declared exactly once in all of the configurations. If no
repository is given, skip this check.
Raise ValueError if the repository is not found in a configuration, or is declared multiple
times.
'''
if not repository:
count = len(
tuple(
config_repository
for config in configurations.values()
for config_repository in config['location']['repositories']
)
)
if count > 1:
raise ValueError(
'Can\'t determine which repository to use. Use --repository option to disambiguate'
)
return
count = len(
@ -176,3 +165,26 @@ def guard_configuration_contains_repository(repository, configurations):
raise ValueError('Repository {} not found in configuration files'.format(repository))
if count > 1:
raise ValueError('Repository {} found in multiple configuration files'.format(repository))
def guard_single_repository_selected(repository, configurations):
'''
Given a repository path and a dict mapping from config filename to corresponding parsed config
dict, ensure either a single repository exists across all configuration files or a repository
path was given.
'''
if repository:
return
count = len(
tuple(
config_repository
for config in configurations.values()
for config_repository in config['location']['repositories']
)
)
if count != 1:
raise ValueError(
'Can\'t determine which repository to use. Use --repository to disambiguate'
)

View file

@ -51,6 +51,9 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
process with the requested log level. Additionally, raise a CalledProcessError if a process
exits with an error (or a warning for exit code 1, if that process matches the Borg local path).
If output log level is None, then instead of logging, capture output for each process and return
it as a dict from the process to its output.
For simplicity, it's assumed that the output buffer for each process is its stdout. But if any
stdouts are given to exclude, then for any matching processes, log from their stderr instead.
@ -65,6 +68,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
if process.stdout or process.stderr
}
output_buffers = list(process_for_output_buffer.keys())
captured_outputs = collections.defaultdict(list)
# Log output for each process until they all exit.
while True:
@ -99,7 +103,10 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
last_lines.pop(0)
logger.log(output_log_level, line)
if output_log_level is None:
captured_outputs[ready_process].append(line)
else:
logger.log(output_log_level, line)
still_running = False
@ -133,6 +140,11 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
if not still_running:
break
if captured_outputs:
return {
process: '\n'.join(output_lines) for process, output_lines in captured_outputs.items()
}
def log_command(full_command, input_file, output_file):
'''
@ -222,13 +234,14 @@ def execute_command_with_processes(
run as well. This is useful, for instance, for processes that are streaming output to a named
pipe that the given command is consuming from.
If an open output file object is given, then write stdout to the file and only log stderr (but
only if an output log level is set). If an open input file object is given, then read stdin from
the file. If shell is True, execute the command within a shell. If an extra environment dict is
given, then use it to augment the current environment, and pass the result into the command. If
a working directory is given, use that as the present working directory when running the
command. If a Borg local path is given, then for any matching command or process (regardless of
arguments), treat exit code 1 as a warning instead of an error.
If an open output file object is given, then write stdout to the file and only log stderr. But
if output log level is None, instead suppress logging and return the captured output for (only)
the given command. If an open input file object is given, then read stdin from the file. If
shell is True, execute the command within a shell. If an extra environment dict is given, then
use it to augment the current environment, and pass the result into the command. If a working
directory is given, use that as the present working directory when running the command. If a
Borg local path is given, then for any matching command or process (regardless of arguments),
treat exit code 1 as a warning instead of an error.
Raise subprocesses.CalledProcessError if an error occurs while running the command or in the
upstream process.
@ -259,9 +272,12 @@ def execute_command_with_processes(
process.kill()
raise
log_outputs(
captured_outputs = log_outputs(
tuple(processes) + (command_process,),
(input_file, output_file),
output_log_level,
borg_local_path=borg_local_path,
)
if output_log_level is None:
return captured_outputs.get(command_process)

View file

@ -125,7 +125,9 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
try:
response = requests.post(ping_url, data=payload.encode('utf-8'))
response = requests.post(
ping_url, data=payload.encode('utf-8'), verify=hook_config.get('verify_tls', True)
)
if not response.ok:
response.raise_for_status()
except requests.exceptions.RequestException as error:

View file

@ -131,12 +131,13 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
borg_local_path=location_config.get('local_path', 'borg'),
)

View file

@ -166,11 +166,12 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
extra_environment=extra_environment,
borg_local_path=location_config.get('local_path', 'borg'),
)

View file

@ -168,12 +168,13 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
extra_environment=extra_environment,
borg_local_path=location_config.get('local_path', 'borg'),
)
execute_command(analyze_command, extra_environment=extra_environment)

View file

@ -4,7 +4,7 @@ COPY . /app
RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt \
&& for action in init prune compact create check extract export-tar mount umount restore list info borg; do \
&& for action in rcreate transfer prune compact create check extract export-tar mount umount restore rlist list rinfo info borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic "$action" --help >> /command-line.txt; done

View file

@ -530,3 +530,11 @@ main .elv-toc + h1 .direct-link {
display: none ;
}
}
.header-anchor {
text-decoration: none;
}
.header-anchor:hover::after {
content: " 🔗";
}

View file

@ -40,6 +40,13 @@ There are additional hooks that run before/after other actions as well. For
instance, `before_prune` runs before a `prune` action for a repository, while
`after_prune` runs after it.
<span class="minilink minilink-addedin">New in version 1.7.0</span> The
`before_actions` and `after_actions` hooks run before/after all the actions
(like `create`, `prune`, etc.) for each repository. These hooks are a good
place to run per-repository steps like mounting/unmounting a remote
filesystem.
## Variable interpolation
The before and after action hooks support interpolating particular runtime

View file

@ -76,7 +76,7 @@ location:
- /home
repositories:
- me@buddys-server.org:backup.borg
- ssh://me@buddys-server.org/./backup.borg
hooks:
before_backup:

View file

@ -52,6 +52,8 @@ hooks:
postgresql_databases:
- name: users
hostname: database1.example.org
- name: orders
hostname: database2.example.org
port: 5433
username: postgres
password: trustsome1
@ -59,14 +61,14 @@ hooks:
options: "--role=someone"
mysql_databases:
- name: posts
hostname: database2.example.org
hostname: database3.example.org
port: 3307
username: root
password: trustsome1
options: "--skip-comments"
mongodb_databases:
- name: messages
hostname: database3.example.org
hostname: database4.example.org
port: 27018
username: dbuser
password: trustsome1
@ -131,14 +133,13 @@ that you'd like supported.
To restore a database dump from an archive, use the `borgmatic restore`
action. But the first step is to figure out which archive to restore from. A
good way to do that is to use the `list` action:
good way to do that is to use the `rlist` action:
```bash
borgmatic list
borgmatic rlist
```
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
borgmatic!)
(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
That should yield output looking something like:

View file

@ -27,9 +27,6 @@ borgmatic create
borgmatic check
```
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
`--prune`, `--create`, or `--check`. Or upgrade borgmatic!)
You can run with only one of these actions provided, or you can mix and match
any number of them in a single borgmatic run. This supports approaches like
skipping certain actions while running others. For instance, this skips
@ -70,7 +67,9 @@ Here are the available checks from fastest to slowest:
* `extract`: Performs an extraction dry-run of the most recent archive.
* `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data (implies `archives` as well).
See [Borg's check documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html) for more information.
See [Borg's check
documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html)
for more information.
### Check frequency
@ -83,19 +82,26 @@ consistency:
checks:
- name: repository
frequency: 2 weeks
- name: archives
frequency: 1 month
```
This tells borgmatic to run this consistency check at most once every two
weeks for a given repository. The `frequency` value is a number followed by a
unit of time, e.g. "3 days", "1 week", "2 months", etc. The `frequency`
defaults to "always", which means run this check every time checks run.
This tells borgmatic to run the `repository` consistency check at most once
every two weeks for a given repository and the `archives` check at most once a
month. The `frequency` value is a number followed by a unit of time, e.g. "3
days", "1 week", "2 months", etc. The `frequency` defaults to `always`, which
means run this check every time checks run.
Unlike a real scheduler like cron, borgmatic only makes a best effort to run
checks on the configured frequency. It compares that frequency with how long
it's been since the last check for a given repository (as recorded in a file
within `~/.borgmatic/checks`). If it hasn't been long enough, the check is
skipped. And you still have to run `borgmatic check` (or just `borgmatic`) in
order for checks to run, even when a `frequency` is configured!
skipped. And you still have to run `borgmatic check` (or `borgmatic` without
actions) in order for checks to run, even when a `frequency` is configured!
This also applies *across* configuration files that have the same repository
configured. Make sure you have the same check frequency configured in each
though—or the most frequently configured check will apply.
If you want to temporarily ignore your configured frequencies, you can invoke
`borgmatic check --force` to run checks unconditionally.

View file

@ -9,14 +9,13 @@ eleventyNavigation:
When the worst happens—or you want to test your backups—the first step is
to figure out which archive to extract. A good way to do that is to use the
`list` action:
`rlist` action:
```bash
borgmatic list
borgmatic rlist
```
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
borgmatic!)
(No borgmatic `rlist` action? Try `list` instead or upgrade borgmatic!)
That should yield output looking something like:
@ -32,10 +31,9 @@ and therefore the latest timestamp, run a command like:
borgmatic extract --archive host-2019-01-02T04:06:07.080910
```
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
borgmatic!)
(No borgmatic `extract` action? Upgrade borgmatic!)
With newer versions of borgmatic, you can simplify this to:
Or simplify this to:
```bash
borgmatic extract --archive latest
@ -43,7 +41,8 @@ borgmatic extract --archive latest
The `--archive` value is the name of the archive to extract. This extracts the
entire contents of the archive to the current directory, so make sure you're
in the right place before running the command.
in the right place before running the command—or see below about the
`--destination` flag.
## Repository selection
@ -65,13 +64,15 @@ everything from an archive. To do that, tack on one or more `--path` values.
For instance:
```bash
borgmatic extract --archive host-2019-... --path path/1 path/2
borgmatic extract --archive latest --path path/1 path/2
```
Note that the specified restore paths should not have a leading slash. Like a
whole-archive extract, this also extracts into the current directory. So for
example, if you happen to be in the directory `/var` and you run the `extract`
command above, borgmatic will extract `/var/path/1` and `/var/path/2`.
whole-archive extract, this also extracts into the current directory by
default. So for example, if you happen to be in the directory `/var` and you
run the `extract` command above, borgmatic will extract `/var/path/1` and
`/var/path/2`.
## Extract to a particular destination
@ -80,7 +81,7 @@ extract files to a particular destination directory, use the `--destination`
flag:
```bash
borgmatic extract --archive host-2019-... --destination /tmp
borgmatic extract --archive latest --destination /tmp
```
When using the `--destination` flag, be careful not to overwrite your system's
@ -104,7 +105,7 @@ archive as a [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace)
filesystem, you can use the `borgmatic mount` action. Here's an example:
```bash
borgmatic mount --archive host-2019-... --mount-point /mnt
borgmatic mount --archive latest --mount-point /mnt
```
This mounts the entire archive on the given mount point `/mnt`, so that you
@ -127,7 +128,7 @@ your archive, use the `--path` flag, similar to the `extract` action above.
For instance:
```bash
borgmatic mount --archive host-2019-... --mount-point /mnt --path var/lib
borgmatic mount --archive latest --mount-point /mnt --path var/lib
```
When you're all done exploring your files, unmount your mount point. No

View file

@ -37,18 +37,34 @@ borgmatic --stats
## Existing backups
borgmatic provides convenient actions for Borg's
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
[`list`](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
[`info`](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
functionality:
```bash
borgmatic list
borgmatic info
```
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
`--info`. Or upgrade borgmatic!)
You can change the output format of `borgmatic list` by specifying your own
with `--format`. Refer to the [borg list --format
documentation](https://borgbackup.readthedocs.io/en/stable/usage/list.html#the-format-specifier-syntax)
for available values.
*(No borgmatic `list` or `info` actions? Upgrade borgmatic!)*
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
There are also `rlist` and `rinfo` actions for displaying repository
information with Borg 2.x:
```bash
borgmatic rlist
borgmatic rinfo
```
See the [borgmatic command-line
reference](https://torsion.org/borgmatic/docs/reference/command-line/) for
more information.
### Searching for a file

View file

@ -20,8 +20,8 @@ location:
# Paths of local or remote repositories to backup to.
repositories:
- 1234@usw-s001.rsync.net:backups.borg
- k8pDxu32@k8pDxu32.repo.borgbase.com:repo
- ssh://1234@usw-s001.rsync.net/./backups.borg
- ssh://k8pDxu32@k8pDxu32.repo.borgbase.com/./repo
- /var/lib/backups/local.borg
```

View file

@ -8,13 +8,15 @@ eleventyNavigation:
## Multiple backup configurations
You may find yourself wanting to create different backup policies for
different applications on your system. For instance, you may want one backup
configuration for your database data directory, and a different configuration
for your user home directories.
different applications on your system or even for different backup
repositories. For instance, you might want one backup configuration for your
database data directory and a different configuration for your user home
directories. Or one backup configuration for your local backups with a
different configuration for your remote repository.
The way to accomplish that is pretty simple: Create multiple separate
configuration files and place each one in a `/etc/borgmatic.d/` directory. For
instance:
instance, for applications:
```bash
sudo mkdir /etc/borgmatic.d
@ -22,6 +24,14 @@ sudo generate-borgmatic-config --destination /etc/borgmatic.d/app1.yaml
sudo generate-borgmatic-config --destination /etc/borgmatic.d/app2.yaml
```
Or, for repositories:
```bash
sudo mkdir /etc/borgmatic.d
sudo generate-borgmatic-config --destination /etc/borgmatic.d/repo1.yaml
sudo generate-borgmatic-config --destination /etc/borgmatic.d/repo2.yaml
```
When you set up multiple configuration files like this, borgmatic will run
each one in turn from a single borgmatic invocation. This includes, by
default, the traditional `/etc/borgmatic/config.yaml` as well.
@ -29,7 +39,8 @@ default, the traditional `/etc/borgmatic/config.yaml` as well.
Each configuration file is interpreted independently, as if you ran borgmatic
for each configuration file one at a time. In other words, borgmatic does not
perform any merging of configuration files by default. If you'd like borgmatic
to merge your configuration files, see below about configuration includes.
to merge your configuration files, for instance to avoid duplication of
settings, see below about configuration includes.
Additionally, the `~/.config/borgmatic.d/` directory works the same way as
`/etc/borgmatic.d`.

View file

@ -158,9 +158,9 @@ itself. But the logs are only included for errors that occur when a `prune`,
`compact`, `create`, or `check` action is run.
You can customize the verbosity of the logs that are sent to Healthchecks with
borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags
may also be of use. See `borgmatic --help` for more information. Additionally,
see the [borgmatic configuration
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags
may also be of use. See `borgmatic create --help` for more information.
Additionally, see the [borgmatic configuration
file](https://torsion.org/borgmatic/docs/reference/configuration/) for
additional Healthchecks options.
@ -319,8 +319,8 @@ hooks:
## Scripting borgmatic
To consume the output of borgmatic in other software, you can include an
optional `--json` flag with `create`, `list`, or `info` to get the output
formatted as JSON.
optional `--json` flag with `create`, `rlist`, `rinfo`, or `info` to get the
output formatted as JSON.
Note that when you specify the `--json` flag, Borg's other non-JSON output is
suppressed so as not to interfere with the captured JSON. Also note that JSON
@ -329,9 +329,9 @@ output only shows up at the console, and not in syslog.
### Latest backups
All borgmatic actions that accept an "--archive" flag allow you to specify an
archive name of "latest". This lets you get the latest archive without having
to first run "borgmatic list" manually, which can be handy in automated
All borgmatic actions that accept an `--archive` flag allow you to specify an
archive name of `latest`. This lets you get the latest archive without having
to first run `borgmatic rlist` manually, which can be handy in automated
scripts. Here's an example:
```bash

View file

@ -25,8 +25,8 @@ storage:
```
This uses the `MY_PASSPHRASE` environment variable as your encryption
passphrase. Note that the `{` `}` brackets are required. Just `$MY_PASSPHRASE`
will not work.
passphrase. Note that the `{` `}` brackets are required. `$MY_PASSPHRASE` by
itself will not work.
In the case of `encryption_passphrase` in particular, an alternate approach
is to use Borg's `BORG_PASSPHRASE` environment variable, which doesn't even

View file

@ -46,12 +46,11 @@ options, as that part is provided by borgmatic.
You can also specify Borg options for relevant commands:
```bash
borgmatic borg list --progress
borgmatic borg rlist --short
```
This runs Borg's `list` command once on each configured borgmatic
repository. However, the native `borgmatic list` action should be preferred
for most use.
This runs Borg's `rlist` command once on each configured borgmatic repository.
(The native `borgmatic rlist` action should be preferred for most use.)
What if you only want to run Borg on a single configured borgmatic repository
when you've got several configured? Not a problem.
@ -63,7 +62,7 @@ borgmatic borg --repository repo.borg break-lock
And what about a single archive?
```bash
borgmatic borg --archive your-archive-name list
borgmatic borg --archive your-archive-name rlist
```
### Limitations

View file

@ -186,32 +186,39 @@ files via configuration management, or you want to double check that your hand
edits are valid.
## Initialization
## Repository creation
Before you can create backups with borgmatic, you first need to initialize a
Borg repository so you have a destination for your backup archives. (But skip
this step if you already have a Borg repository.) To create a repository, run
a command like the following:
Before you can create backups with borgmatic, you first need to create a Borg
repository so you have a destination for your backup archives. (But skip this
step if you already have a Borg repository.) To create a repository, run a
command like the following with Borg 1.x:
```bash
sudo borgmatic init --encryption repokey
```
(No borgmatic `init` action? Try the old-style `--init` flag, or upgrade
borgmatic!)
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
Or, with Borg 2.x:
```bash
sudo borgmatic rcreate --encryption repokey-aes-ocb
```
(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on
certain platforms like ARM64.)
This uses the borgmatic configuration file you created above to determine
which local or remote repository to create, and encrypts it with the
encryption passphrase specified there if one is provided. Read about [Borg
encryption
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-modes)
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-mode-tldr)
for the menu of available encryption modes.
Also, optionally check out the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/stable/quickstart.html) for more
background about repository initialization.
background about repository creation.
Note that borgmatic skips repository initialization if the repository already
Note that borgmatic skips repository creation if the repository already
exists. This supports use cases like ensuring a repository exists prior to
performing a backup.
@ -221,21 +228,21 @@ key-based SSH access to the desired user account on the remote host.
## Backups
Now that you've configured borgmatic and initialized a repository, it's a
good idea to test that borgmatic is working. So to run borgmatic and start a
Now that you've configured borgmatic and created a repository, it's a good
idea to test that borgmatic is working. So to run borgmatic and start a
backup, you can invoke it like this:
```bash
sudo borgmatic create --verbosity 1 --files --stats
sudo borgmatic create --verbosity 1 --list --stats
```
(No borgmatic `--files` flag? It's only present in newer versions of
borgmatic. So try leaving it out, or upgrade borgmatic!)
(No borgmatic `--list` flag? Try `--files` instead, leave it out, or upgrade
borgmatic!)
The `--verbosity` flag makes borgmatic show the steps it's performing. The
`--files` flag lists each file that's new or changed since the last backup.
And `--stats` shows summary information about the created archive. All of
these flags are optional.
`--list` flag lists each file that's new or changed since the last backup. And
`--stats` shows summary information about the created archive. All of these
flags are optional.
As the command runs, you should eyeball the output to see if it matches your
expectations based on your configuration.
@ -255,7 +262,7 @@ backup, *and* `check` backups for consistency problems due to things like file
damage. For instance:
```bash
sudo borgmatic --verbosity 1 --files --stats
sudo borgmatic --verbosity 1 --list --stats
```
## Autopilot
@ -340,7 +347,7 @@ instead:
sudo su -c "borgmatic --bash-completion > /usr/share/bash-completion/completions/borgmatic"
```
Or, if you'd like to install the script for just the current user:
Or, if you'd like to install the script for only the current user:
```bash
mkdir --parents ~/.local/share/bash-completion/completions

View file

@ -1,11 +1,11 @@
---
title: How to upgrade borgmatic
title: How to upgrade borgmatic and Borg
eleventyNavigation:
key: 📦 Upgrade borgmatic
key: 📦 Upgrade borgmatic/Borg
parent: How-to guides
order: 12
---
## Upgrading
## Upgrading borgmatic
In general, all you should need to do to upgrade borgmatic is run the
following:
@ -115,3 +115,84 @@ sudo pip3 install --user borgmatic
That's it! borgmatic will continue using your /etc/borgmatic configuration
files.
## Upgrading Borg
To upgrade to a new version of Borg, you can generally install a new version
the same way you installed the previous version, paying attention to any
instructions included with each Borg release changelog linked from the
[releases page](https://github.com/borgbackup/borg/releases). Some more major
Borg releases require additional steps that borgmatic can help with.
### Borg 1.2 to 2.0
<span class="minilink minilink-addedin">New in borgmatic version 1.7.0</span>
Upgrading Borg from 1.2 to 2.0 requires manually upgrading your existing Borg
1 repositories before use with Borg or borgmatic. Here's how you can
accomplish that.
Start by upgrading borgmatic as described above to at least version 1.7.0 and
Borg to 2.0. Then, rename your repository in borgmatic's configuration file to
a new repository path. The repository upgrade process does not occur
in-place; you'll create a new repository with a copy of your old repository's
data.
Let's say your original borgmatic repository configuration file looks something
like this:
```yaml
location:
repositories:
- original.borg
```
Change it to a new (not yet created) repository path:
```yaml
location:
repositories:
- upgraded.borg
```
Then, run the `rcreate` action (formerly `init`) to create that new Borg 2
repository:
```bash
borgmatic rcreate --verbosity 1 --encryption repokey-aes-ocb \
--source-repository original.borg --repository upgraded.borg
```
(Note that `repokey-chacha20-poly1305` may be faster than `repokey-aes-ocb` on
certain platforms like ARM64.)
This creates an empty repository and doesn't actually transfer any data yet.
The `--source-repository` flag is necessary to reuse key material from your
Borg 1 repository so that the subsequent data transfer can work.
To transfer data from your original Borg 1 repository to your newly created
Borg 2 repository:
```bash
borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \
original.borg --repository upgraded.borg --dry-run
borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \
original.borg --repository upgraded.borg
borgmatic transfer --verbosity 1 --upgrader From12To20 --source-repository \
original.borg --repository upgraded.borg --dry-run
```
The first command with `--dry-run` tells you what Borg is going to do during
the transfer, the second command actually performs the transfer/upgrade (this
might take a while), and the final command with `--dry-run` again provides
confirmation of success—or tells you if something hasn't been transferred yet.
Note that by omitting the `--upgrader` flag, you can also do archive transfers
between Borg 2 repositories without upgrading, even down to individual
archives. For more on that functionality, see the [Borg transfer
documentation](https://borgbackup.readthedocs.io/en/2.0.0b1/usage/transfer.html).
That's it! Now you can use your new Borg 2 repository as normal with
borgmatic. If you've got multiple repositories, repeat the above process for
each.

View file

@ -14,8 +14,8 @@ apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client m
py3-ruamel.yaml py3-ruamel.yaml.clib bash
# If certain dependencies of black are available in this version of Alpine, install them.
apk add --no-cache py3-typed-ast py3-regex || true
python3 -m pip install --no-cache --upgrade pip==22.0.3 setuptools==60.8.1
pip3 install tox==3.24.5
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1
pip3 install --ignore-installed tox==3.25.1
export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox --sitepackages
tox --workdir /tmp/.tox --sitepackages -e end-to-end

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.6.5'
VERSION = '1.7.0'
setup(

View file

@ -0,0 +1,16 @@
import os
import subprocess
import tempfile
def test_generate_borgmatic_config_with_merging_succeeds():
with tempfile.TemporaryDirectory() as temporary_directory:
config_path = os.path.join(temporary_directory, 'test.yaml')
new_config_path = os.path.join(temporary_directory, 'new.yaml')
subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
subprocess.check_call(
f'generate-borgmatic-config --source {config_path} --destination {new_config_path}'.split(
' '
)
)

View file

@ -0,0 +1,58 @@
import os
import shutil
import subprocess
import tempfile
def generate_configuration(config_path, repository_path):
'''
Generate borgmatic configuration into a file at the config path, and update the defaults so as
to work for testing (including injecting the given repository path and tacking on an encryption
passphrase).
'''
subprocess.check_call(
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
)
config = (
open(config_path)
.read()
.replace('user@backupserver:sourcehostname.borg', repository_path)
.replace('- user@backupserver:{fqdn}', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', '- {}'.format(config_path))
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')
+ 'storage:\n encryption_passphrase: "test"'
)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_override_get_normalized():
temporary_directory = tempfile.mkdtemp()
repository_path = os.path.join(temporary_directory, 'test.borg')
extract_path = os.path.join(temporary_directory, 'extract')
original_working_directory = os.getcwd()
os.mkdir(extract_path)
os.chdir(extract_path)
try:
config_path = os.path.join(temporary_directory, 'test.yaml')
generate_configuration(config_path, repository_path)
subprocess.check_call(
f'borgmatic -v 2 --config {config_path} init --encryption repokey'.split(' ')
)
# Run borgmatic with an override structured for an outdated config file format. If
# normalization is working, it should get normalized and shouldn't error.
subprocess.check_call(
f'borgmatic create --config {config_path} --override hooks.healthchecks=http://localhost:8888/someuuid'.split(
' '
)
)
finally:
os.chdir(original_working_directory)
shutil.rmtree(temporary_directory)

View file

@ -107,13 +107,6 @@ def test_parse_arguments_with_list_json_overrides_default():
assert arguments['list'].json is True
def test_parse_arguments_with_dashed_list_json_overrides_default():
arguments = module.parse_arguments('--list', '--json')
assert 'list' in arguments
assert arguments['list'].json is True
def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -127,14 +120,14 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--stats', '--files')
arguments = module.parse_arguments('--stats', '--list')
assert 'prune' in arguments
assert arguments['prune'].stats
assert arguments['prune'].files
assert arguments['prune'].list_archives
assert 'create' in arguments
assert arguments['create'].stats
assert arguments['create'].files
assert arguments['create'].list_files
assert 'check' in arguments
@ -191,16 +184,6 @@ def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
assert 'check' not in arguments
def test_parse_arguments_with_dashed_prune_action_leaves_other_actions_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--prune')
assert 'prune' in arguments
assert 'create' not in arguments
assert 'check' not in arguments
def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -211,16 +194,6 @@ def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
assert 'check' in arguments
def test_parse_arguments_with_multiple_dashed_actions_leaves_other_action_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--create', '--check')
assert 'prune' not in arguments
assert 'create' in arguments
assert 'check' in arguments
def test_parse_arguments_with_invalid_arguments_exits():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -248,12 +221,6 @@ def test_parse_arguments_allows_encryption_mode_with_init():
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey')
def test_parse_arguments_allows_encryption_mode_with_dashed_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey')
def test_parse_arguments_requires_encryption_mode_with_init():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -287,15 +254,6 @@ def test_parse_arguments_allows_init_and_create():
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create')
def test_parse_arguments_disallows_init_and_dry_run():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments(
'--config', 'myconfig', 'init', '--encryption', 'repokey', '--dry-run'
)
def test_parse_arguments_disallows_repository_unless_action_consumes_it():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -361,24 +319,12 @@ def test_parse_arguments_allows_archive_with_mount():
)
def test_parse_arguments_allows_archive_with_dashed_extract():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test')
def test_parse_arguments_allows_archive_with_restore():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', 'restore', '--archive', 'test')
def test_parse_arguments_allows_archive_with_dashed_restore():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--config', 'myconfig', '--restore', '--archive', 'test')
def test_parse_arguments_allows_archive_with_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -457,23 +403,23 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
module.parse_arguments('--stats', 'list')
def test_parse_arguments_with_files_and_create_flags_does_not_raise():
def test_parse_arguments_with_list_and_create_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--files', 'create', 'list')
module.parse_arguments('--list', 'create')
def test_parse_arguments_with_files_and_prune_flags_does_not_raise():
def test_parse_arguments_with_list_and_prune_flags_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--files', 'prune', 'list')
module.parse_arguments('--list', 'prune')
def test_parse_arguments_with_files_flag_but_no_create_or_prune_or_restore_flag_raises_value_error():
def test_parse_arguments_with_list_flag_but_no_relevant_action_raises_value_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--files', 'list')
module.parse_arguments('--list', 'rcreate')
def test_parse_arguments_allows_json_with_list_or_info():
@ -483,12 +429,6 @@ def test_parse_arguments_allows_json_with_list_or_info():
module.parse_arguments('info', '--json')
def test_parse_arguments_allows_json_with_dashed_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--info', '--json')
def test_parse_arguments_disallows_json_with_both_list_and_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@ -496,6 +436,56 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
module.parse_arguments('list', 'info', '--json')
def test_parse_arguments_disallows_json_with_both_list_and_rinfo():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('list', 'rinfo', '--json')
def test_parse_arguments_disallows_json_with_both_rinfo_and_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('rinfo', 'info', '--json')
def test_parse_arguments_disallows_transfer_with_both_archive_and_glob_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments(
'transfer',
'--source-repository',
'source.borg',
'--archive',
'foo',
'--glob-archives',
'*bar',
)
def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--archive', 'foo', '--glob-archives', '*bar')
def test_parse_arguments_disallows_info_with_both_archive_and_prefix():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--archive', 'foo', '--prefix', 'bar')
def test_parse_arguments_disallows_info_with_both_prefix_and_glob_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--prefix', 'foo', '--glob-archives', '*bar')
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])

View file

@ -60,39 +60,39 @@ def test_parse_configuration_transforms_file_into_mapping():
'''
)
result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert result == {
assert config == {
'location': {'source_directories': ['/home', '/etc'], 'repositories': ['hostname.borg']},
'retention': {'keep_daily': 7, 'keep_hourly': 24, 'keep_minutely': 60},
'consistency': {'checks': [{'name': 'repository'}, {'name': 'archives'}]},
}
assert logs == []
def test_parse_configuration_passes_through_quoted_punctuation():
escaped_punctuation = string.punctuation.replace('\\', r'\\').replace('"', r'\"')
mock_config_and_schema(
'''
f'''
location:
source_directories:
- /home
- "/home/{escaped_punctuation}"
repositories:
- "{}.borg"
'''.format(
escaped_punctuation
)
- test.borg
'''
)
result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert result == {
assert config == {
'location': {
'source_directories': ['/home'],
'repositories': ['{}.borg'.format(string.punctuation)],
'source_directories': [f'/home/{string.punctuation}'],
'repositories': ['test.borg'],
}
}
assert logs == []
def test_parse_configuration_with_schema_lacking_examples_does_not_raise():
@ -148,12 +148,13 @@ def test_parse_configuration_inlines_include():
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert result == {
assert config == {
'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']},
'retention': {'keep_daily': 7, 'keep_hourly': 24},
}
assert logs == []
def test_parse_configuration_merges_include():
@ -181,12 +182,13 @@ def test_parse_configuration_merges_include():
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert result == {
assert config == {
'location': {'source_directories': ['/home'], 'repositories': ['hostname.borg']},
'retention': {'keep_daily': 1, 'keep_hourly': 24},
}
assert logs == []
def test_parse_configuration_raises_for_missing_config_file():
@ -238,17 +240,18 @@ def test_parse_configuration_applies_overrides():
'''
)
result = module.parse_configuration(
config, logs = module.parse_configuration(
'/tmp/config.yaml', '/tmp/schema.yaml', overrides=['location.local_path=borg2']
)
assert result == {
assert config == {
'location': {
'source_directories': ['/home'],
'repositories': ['hostname.borg'],
'local_path': 'borg2',
}
}
assert logs == []
def test_parse_configuration_applies_normalization():
@ -265,12 +268,13 @@ def test_parse_configuration_applies_normalization():
'''
)
result = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
assert result == {
assert config == {
'location': {
'source_directories': ['/home'],
'repositories': ['hostname.borg'],
'exclude_if_present': ['.nobackup'],
}
}
assert logs == []

View file

@ -54,6 +54,30 @@ def test_log_outputs_skips_logs_for_process_with_none_stdout():
)
def test_log_outputs_returns_output_without_logging_for_output_log_level_none():
flexmock(module.logger).should_receive('log').never()
flexmock(module).should_receive('exit_code_indicates_error').and_return(False)
hi_process = subprocess.Popen(['echo', 'hi'], stdout=subprocess.PIPE)
flexmock(module).should_receive('output_buffer_for_process').with_args(
hi_process, ()
).and_return(hi_process.stdout)
there_process = subprocess.Popen(['echo', 'there'], stdout=subprocess.PIPE)
flexmock(module).should_receive('output_buffer_for_process').with_args(
there_process, ()
).and_return(there_process.stdout)
captured_outputs = module.log_outputs(
(hi_process, there_process),
exclude_stdouts=(),
output_log_level=None,
borg_local_path='borg',
)
assert captured_outputs == {hi_process: 'hi', there_process: 'there'}
def test_log_outputs_includes_error_output_in_exception():
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)

View file

@ -8,6 +8,8 @@ from ..test_verbosity import insert_logging_mock
def test_run_arbitrary_borg_calls_borg_with_parameters():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo'),
@ -17,11 +19,13 @@ def test_run_arbitrary_borg_calls_borg_with_parameters():
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'],
)
def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--info'),
@ -32,11 +36,13 @@ def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
insert_logging_mock(logging.INFO)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'],
)
def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
@ -47,12 +53,16 @@ def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
insert_logging_mock(logging.DEBUG)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=['break-lock'],
)
def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(
('--lock-wait', '5')
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--lock-wait', '5'),
@ -62,12 +72,18 @@ def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters(
)
module.run_arbitrary_borg(
repository='repo', storage_config=storage_config, options=['break-lock'],
repository='repo',
storage_config=storage_config,
local_borg_version='1.2.3',
options=['break-lock'],
)
def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
storage_config = {}
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo::archive'),
@ -77,11 +93,17 @@ def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
)
module.run_arbitrary_borg(
repository='repo', storage_config=storage_config, options=['break-lock'], archive='archive',
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['break-lock'],
archive='archive',
)
def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg1', 'break-lock', 'repo'),
@ -91,11 +113,19 @@ def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'], local_path='borg1',
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['break-lock'],
local_path='borg1',
)
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(
('--remote-path', 'borg1')
).and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
@ -105,11 +135,17 @@ def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_paramet
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'], remote_path='borg1',
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['break-lock'],
remote_path='borg1',
)
def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo', '--progress'),
@ -119,11 +155,16 @@ def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['list', '--progress'],
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['list', '--progress'],
)
def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo'),
@ -133,22 +174,29 @@ def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['--', 'break-lock'],
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['--', 'break-lock'],
)
def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
flexmock(module.flags).should_receive('make_repository_flags').never()
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg',), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None,
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=[],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=[],
)
def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'key', 'export', 'repo'),
@ -158,11 +206,13 @@ def test_run_arbitrary_borg_passes_key_sub_command_to_borg_before_repository():
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['key', 'export'],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=['key', 'export'],
)
def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'debug', 'dump-manifest', 'repo', 'path'),
@ -172,11 +222,16 @@ def test_run_arbitrary_borg_passes_debug_sub_command_to_borg_before_repository()
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['debug', 'dump-manifest', 'path'],
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['debug', 'dump-manifest', 'path'],
)
def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repository():
flexmock(module.flags).should_receive('make_repository_flags').never()
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'debug', 'info'),
@ -186,11 +241,13 @@ def test_run_arbitrary_borg_with_debug_info_command_does_not_pass_borg_repositor
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['debug', 'info'],
repository='repo', storage_config={}, local_borg_version='1.2.3', options=['debug', 'info'],
)
def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_borg_repository():
flexmock(module.flags).should_receive('make_repository_flags').never()
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'debug', 'convert-profile', 'in', 'out'),
@ -200,5 +257,8 @@ def test_run_arbitrary_borg_with_debug_convert_profile_command_does_not_pass_bor
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['debug', 'convert-profile', 'in', 'out'],
repository='repo',
storage_config={},
local_borg_version='1.2.3',
options=['debug', 'convert-profile', 'in', 'out'],
)

View file

@ -49,18 +49,6 @@ def test_parse_checks_with_disabled_returns_no_checks():
assert checks == ()
def test_parse_checks_with_data_check_also_injects_archives():
checks = module.parse_checks({'checks': [{'name': 'data'}]})
assert checks == ('data', 'archives')
def test_parse_checks_with_data_check_passes_through_archives():
checks = module.parse_checks({'checks': [{'name': 'data'}, {'name': 'archives'}]})
assert checks == ('data', 'archives')
def test_parse_checks_prefers_override_checks_to_configured_checks():
checks = module.parse_checks(
{'checks': [{'name': 'archives'}]}, only_checks=['repository', 'extract']
@ -69,12 +57,6 @@ def test_parse_checks_prefers_override_checks_to_configured_checks():
assert checks == ('repository', 'extract')
def test_parse_checks_with_override_data_check_also_injects_archives():
checks = module.parse_checks({'checks': [{'name': 'extract'}]}, only_checks=['data'])
assert checks == ('data', 'archives')
@pytest.mark.parametrize(
'frequency,expected_result',
(
@ -217,10 +199,10 @@ def test_make_check_flags_with_archives_check_returns_flag():
assert flags == ('--archives-only',)
def test_make_check_flags_with_data_check_returns_flag():
def test_make_check_flags_with_data_check_returns_flag_and_implies_archives():
flags = module.make_check_flags(('data',))
assert flags == ('--verify-data',)
assert flags == ('--archives-only', '--verify-data',)
def test_make_check_flags_with_extract_omits_extract_flag():
@ -229,10 +211,16 @@ def test_make_check_flags_with_extract_omits_extract_flag():
assert flags == ()
def test_make_check_flags_with_repository_and_data_checks_does_not_return_repository_only():
flags = module.make_check_flags(('repository', 'data',))
assert flags == ('--verify-data',)
def test_make_check_flags_with_default_checks_and_default_prefix_returns_default_flags():
flags = module.make_check_flags(('repository', 'archives'), prefix=module.DEFAULT_PREFIX)
assert flags == ('--prefix', module.DEFAULT_PREFIX)
assert flags == ('--glob-archives', f'{module.DEFAULT_PREFIX}*')
def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_flags():
@ -240,7 +228,7 @@ def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_fla
('repository', 'archives', 'extract'), prefix=module.DEFAULT_PREFIX
)
assert flags == ('--prefix', module.DEFAULT_PREFIX)
assert flags == ('--glob-archives', f'{module.DEFAULT_PREFIX}*')
def test_make_check_flags_with_archives_check_and_last_includes_last_flag():
@ -261,34 +249,34 @@ def test_make_check_flags_with_default_checks_and_last_includes_last_flag():
assert flags == ('--last', '3')
def test_make_check_flags_with_archives_check_and_prefix_includes_prefix_flag():
def test_make_check_flags_with_archives_check_and_prefix_includes_glob_archives_flag():
flags = module.make_check_flags(('archives',), prefix='foo-')
assert flags == ('--archives-only', '--prefix', 'foo-')
assert flags == ('--archives-only', '--glob-archives', 'foo-*')
def test_make_check_flags_with_archives_check_and_empty_prefix_omits_prefix_flag():
def test_make_check_flags_with_archives_check_and_empty_prefix_omits_glob_archives_flag():
flags = module.make_check_flags(('archives',), prefix='')
assert flags == ('--archives-only',)
def test_make_check_flags_with_archives_check_and_none_prefix_omits_prefix_flag():
def test_make_check_flags_with_archives_check_and_none_prefix_omits_glob_archives_flag():
flags = module.make_check_flags(('archives',), prefix=None)
assert flags == ('--archives-only',)
def test_make_check_flags_with_repository_check_and_prefix_omits_prefix_flag():
def test_make_check_flags_with_repository_check_and_prefix_omits_glob_archives_flag():
flags = module.make_check_flags(('repository',), prefix='foo-')
assert flags == ('--repository-only',)
def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag():
def test_make_check_flags_with_default_checks_and_prefix_includes_glob_archives_flag():
flags = module.make_check_flags(('repository', 'archives'), prefix='foo-')
assert flags == ('--prefix', 'foo-')
assert flags == ('--glob-archives', 'foo-*')
def test_read_check_time_does_not_raise():
@ -308,11 +296,12 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
flexmock(module).should_receive('execute_command').never()
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--progress', 'repo'),
@ -327,6 +316,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
progress=True,
)
@ -336,11 +326,12 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
flexmock(module).should_receive('execute_command').never()
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'check', '--repair', 'repo'),
@ -355,6 +346,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
repair=True,
)
@ -373,12 +365,13 @@ def test_check_archives_calls_borg_with_parameters(checks):
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
checks, check_last, module.DEFAULT_PREFIX
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -388,6 +381,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -397,7 +391,7 @@ def test_check_archives_with_json_error_raises():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}'
)
@ -407,6 +401,7 @@ def test_check_archives_with_json_error_raises():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -416,7 +411,7 @@ def test_check_archives_with_missing_json_keys_raises():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return('{invalid JSON')
flexmock(module.rinfo).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.check_archives(
@ -424,6 +419,7 @@ def test_check_archives_with_missing_json_keys_raises():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -433,10 +429,11 @@ def test_check_archives_with_extract_check_calls_extract_only():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').never()
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.extract).should_receive('extract_last_archive_dry_run').once()
flexmock(module).should_receive('write_check_time')
insert_execute_command_never()
@ -446,6 +443,7 @@ def test_check_archives_with_extract_check_calls_extract_only():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -454,10 +452,11 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.INFO)
insert_execute_command_mock(('borg', 'check', '--info', 'repo'))
flexmock(module).should_receive('make_check_time_path')
@ -468,6 +467,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -476,10 +476,11 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_logging_mock(logging.DEBUG)
insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo'))
flexmock(module).should_receive('make_check_time_path')
@ -490,6 +491,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -497,7 +499,7 @@ def test_check_archives_without_any_checks_bails():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(())
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
insert_execute_command_never()
@ -507,6 +509,7 @@ def test_check_archives_without_any_checks_bails():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -516,12 +519,13 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
checks, check_last, module.DEFAULT_PREFIX
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1', 'check', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -531,6 +535,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
local_path='borg1',
)
@ -541,12 +546,13 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
checks, check_last, module.DEFAULT_PREFIX
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -556,6 +562,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
remote_path='borg1',
)
@ -566,12 +573,13 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
checks, check_last, module.DEFAULT_PREFIX
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -581,6 +589,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
location_config={},
storage_config={'lock_wait': 5},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -591,12 +600,13 @@ def test_check_archives_with_retention_prefix():
consistency_config = {'check_last': check_last, 'prefix': prefix}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
checks, check_last, prefix
).and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -606,6 +616,7 @@ def test_check_archives_with_retention_prefix():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -614,10 +625,11 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
flexmock(module).should_receive('make_check_time_path')
flexmock(module).should_receive('write_check_time')
@ -627,4 +639,5 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
location_config={},
storage_config={'extra_borg_options': {'check': '--extra --options'}},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)

View file

@ -21,94 +21,134 @@ COMPACT_COMMAND = ('borg', 'compact')
def test_compact_segments_calls_borg_with_parameters():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO)
module.compact_segments(dry_run=False, repository='repo', storage_config={})
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, local_borg_version='1.2.3'
)
def test_compact_segments_with_log_info_calls_borg_with_info_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--info', 'repo'), logging.INFO)
insert_logging_mock(logging.INFO)
module.compact_segments(repository='repo', storage_config={}, dry_run=False)
module.compact_segments(
repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=False
)
def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
insert_logging_mock(logging.DEBUG)
module.compact_segments(repository='repo', storage_config={}, dry_run=False)
module.compact_segments(
repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=False
)
def test_compact_segments_with_dry_run_skips_borg_call():
flexmock(module).should_receive('execute_command').never()
module.compact_segments(repository='repo', storage_config={}, dry_run=True)
module.compact_segments(
repository='repo', storage_config={}, local_borg_version='1.2.3', dry_run=True
)
def test_compact_segments_with_local_path_calls_borg_via_local_path():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, local_path='borg1',
dry_run=False,
repository='repo',
storage_config={},
local_borg_version='1.2.3',
local_path='borg1',
)
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, remote_path='borg1',
dry_run=False,
repository='repo',
storage_config={},
local_borg_version='1.2.3',
remote_path='borg1',
)
def test_compact_segments_with_progress_calls_borg_with_progress_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, progress=True,
dry_run=False,
repository='repo',
storage_config={},
local_borg_version='1.2.3',
progress=True,
)
def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, cleanup_commits=True,
dry_run=False,
repository='repo',
storage_config={},
local_borg_version='1.2.3',
cleanup_commits=True,
)
def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, threshold=20,
dry_run=False,
repository='repo',
storage_config={},
local_borg_version='1.2.3',
threshold=20,
)
def test_compact_segments_with_umask_calls_borg_with_umask_parameters():
storage_config = {'umask': '077'}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
dry_run=False, repository='repo', storage_config=storage_config, local_borg_version='1.2.3'
)
def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
dry_run=False, repository='repo', storage_config=storage_config, local_borg_version='1.2.3'
)
def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options():
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False,
repository='repo',
storage_config={'extra_borg_options': {'compact': '--extra --options'}},
local_borg_version='1.2.3',
)

View file

@ -277,7 +277,7 @@ def test_borgmatic_source_directories_defaults_when_directory_not_given():
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
ARCHIVE_WITH_PATHS = ('repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar')
REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
def test_create_archive_calls_borg_with_parameters():
@ -292,9 +292,12 @@ def test_create_archive_calls_borg_with_parameters():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ARCHIVE_WITH_PATHS,
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -327,10 +330,13 @@ def test_create_archive_calls_borg_with_environment():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
environment = {'BORG_THINGY': 'YUP'}
flexmock(module.environment).should_receive('make_environment').and_return(environment)
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ARCHIVE_WITH_PATHS,
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -366,9 +372,12 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(pattern_flags)
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS,
('borg', 'create') + pattern_flags + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -404,9 +413,12 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(exclude_flags)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS,
('borg', 'create') + exclude_flags + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -439,9 +451,12 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--info') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -475,9 +490,12 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=None,
output_file=None,
borg_local_path='borg',
@ -512,9 +530,12 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--debug', '--show-rc') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -548,9 +569,12 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=None,
output_file=None,
borg_local_path='borg',
@ -585,9 +609,12 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--dry-run') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -622,9 +649,12 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--info', '--dry-run') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--info', '--dry-run') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -659,9 +689,12 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--checkpoint-interval', '600') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--checkpoint-interval', '600') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -694,9 +727,12 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--chunker-params', '1,2,3,4') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--chunker-params', '1,2,3,4') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -729,9 +765,12 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--compression', 'rle') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--compression', 'rle') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -755,7 +794,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
@pytest.mark.parametrize(
'feature_available,option_flag', ((True, '--upload-ratelimit'), (False, '--remote-ratelimit')),
)
def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_parameters(
def test_create_archive_with_upload_rate_limit_calls_borg_with_upload_ratelimit_parameters(
feature_available, option_flag
):
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
@ -769,9 +808,12 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', option_flag, '100') + ARCHIVE_WITH_PATHS,
('borg', 'create', option_flag, '100') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -787,7 +829,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_upload_ratelimit_
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={'remote_rate_limit': 100},
storage_config={'upload_rate_limit': 100},
local_borg_version='1.2.3',
)
@ -806,9 +848,12 @@ def test_create_archive_with_working_directory_calls_borg_with_working_directory
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ARCHIVE_WITH_PATHS,
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -842,9 +887,12 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--one-file-system') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -869,7 +917,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
@pytest.mark.parametrize(
'feature_available,option_flag', ((True, '--numeric-ids'), (False, '--numeric-owner')),
)
def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter(
def test_create_archive_with_numeric_ids_calls_borg_with_numeric_ids_parameter(
feature_available, option_flag
):
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
@ -883,9 +931,12 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', option_flag) + ARCHIVE_WITH_PATHS,
('borg', 'create', option_flag) + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -899,7 +950,7 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_ids_parameter
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'numeric_owner': True,
'numeric_ids': True,
'exclude_patterns': None,
},
storage_config={},
@ -919,9 +970,12 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--read-special') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -962,9 +1016,12 @@ def test_create_archive_with_basic_option_calls_borg_with_corresponding_paramete
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1009,9 +1066,12 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1042,7 +1102,7 @@ def test_create_archive_with_atime_option_calls_borg_with_corresponding_paramete
(False, False, '--nobsdflags'),
),
)
def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_parameter(
def test_create_archive_with_flags_option_calls_borg_with_corresponding_parameter(
option_value, feature_available, option_flag
):
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
@ -1056,9 +1116,12 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create') + ((option_flag,) if option_flag else ()) + ARCHIVE_WITH_PATHS,
('borg', 'create') + ((option_flag,) if option_flag else ()) + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1072,7 +1135,7 @@ def test_create_archive_with_bsd_flags_option_calls_borg_with_corresponding_para
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'bsd_flags': option_value,
'flags': option_value,
'exclude_patterns': None,
},
storage_config={},
@ -1092,9 +1155,12 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--files-cache', 'ctime,size') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--files-cache', 'ctime,size') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1128,9 +1194,12 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg1', 'create') + ARCHIVE_WITH_PATHS,
('borg1', 'create') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg1',
@ -1164,9 +1233,12 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--remote-path', 'borg1') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--remote-path', 'borg1') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1200,9 +1272,12 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--umask', '740') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1235,9 +1310,12 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--lock-wait', '5') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1270,9 +1348,12 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_o
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.WARNING,
output_file=None,
borg_local_path='borg',
@ -1306,9 +1387,12 @@ def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--info', '--stats') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--info', '--stats') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1343,9 +1427,12 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--list', '--filter', 'AME-') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--list', '--filter', 'AME-') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.WARNING,
output_file=None,
borg_local_path='borg',
@ -1363,7 +1450,7 @@ def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_ou
},
storage_config={},
local_borg_version='1.2.3',
files=True,
list_files=True,
)
@ -1379,9 +1466,12 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--list', '--filter', 'AME-', '--info') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--list', '--filter', 'AME-', '--info') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1400,7 +1490,7 @@ def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_a
},
storage_config={},
local_borg_version='1.2.3',
files=True,
list_files=True,
)
@ -1416,9 +1506,12 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--info', '--progress') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=module.DO_NOT_CAPTURE,
borg_local_path='borg',
@ -1453,9 +1546,12 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=module.DO_NOT_CAPTURE,
borg_local_path='borg',
@ -1490,10 +1586,13 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command_with_processes').with_args(
('borg', 'create', '--one-file-system', '--read-special', '--progress')
+ ARCHIVE_WITH_PATHS,
+ REPO_ARCHIVE_WITH_PATHS,
processes=processes,
output_log_level=logging.INFO,
output_file=module.DO_NOT_CAPTURE,
@ -1529,9 +1628,12 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=None,
output_file=None,
borg_local_path='borg',
@ -1567,9 +1669,12 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=None,
output_file=None,
borg_local_path='borg',
@ -1606,6 +1711,9 @@ def test_create_archive_with_source_directories_glob_expands():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
@ -1642,6 +1750,9 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'),
@ -1678,6 +1789,9 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
@ -1713,6 +1827,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::ARCHIVE_NAME',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'),
@ -1737,6 +1854,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('map_directories_to_devices').and_return({})
@ -1748,9 +1866,12 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(repository_archive_pattern,)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'),
('borg', 'create', repository_archive_pattern, 'foo', 'bar'),
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1772,6 +1893,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
def test_create_archive_with_repository_accepts_borg_placeholders():
repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('map_directories_to_devices').and_return({})
@ -1783,9 +1905,12 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(repository_archive_pattern,)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '{fqdn}::Documents_{hostname}-{now}', 'foo', 'bar'),
('borg', 'create', repository_archive_pattern, 'foo', 'bar'),
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1818,9 +1943,12 @@ def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--extra', '--options') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--extra', '--options') + REPO_ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
output_file=None,
borg_local_path='borg',
@ -1854,9 +1982,12 @@ def test_create_archive_with_stream_processes_calls_borg_with_processes():
flexmock(module).should_receive('ensure_files_readable')
flexmock(module).should_receive('make_pattern_flags').and_return(())
flexmock(module).should_receive('make_exclude_flags').and_return(())
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command_with_processes').with_args(
('borg', 'create', '--one-file-system', '--read-special') + ARCHIVE_WITH_PATHS,
('borg', 'create', '--one-file-system', '--read-special') + REPO_ARCHIVE_WITH_PATHS,
processes=processes,
output_log_level=logging.INFO,
output_file=None,

View file

@ -21,6 +21,9 @@ def insert_execute_command_mock(
def test_export_tar_archive_calls_borg_with_path_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', 'repo::archive', 'test.tar', 'path1', 'path2')
@ -33,10 +36,14 @@ def test_export_tar_archive_calls_borg_with_path_parameters():
paths=['path1', 'path2'],
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
)
def test_export_tar_archive_calls_borg_with_local_path_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg1', 'export-tar', 'repo::archive', 'test.tar'), borg_local_path='borg1'
@ -49,11 +56,15 @@ def test_export_tar_archive_calls_borg_with_local_path_parameters():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
local_path='borg1',
)
def test_export_tar_archive_calls_borg_with_remote_path_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--remote-path', 'borg1', 'repo::archive', 'test.tar')
@ -66,11 +77,15 @@ def test_export_tar_archive_calls_borg_with_remote_path_parameters():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
remote_path='borg1',
)
def test_export_tar_archive_calls_borg_with_umask_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--umask', '0770', 'repo::archive', 'test.tar')
@ -83,10 +98,14 @@ def test_export_tar_archive_calls_borg_with_umask_parameters():
paths=None,
destination_path='test.tar',
storage_config={'umask': '0770'},
local_borg_version='1.2.3',
)
def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--lock-wait', '5', 'repo::archive', 'test.tar')
@ -99,10 +118,14 @@ def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
paths=None,
destination_path='test.tar',
storage_config={'lock_wait': '5'},
local_borg_version='1.2.3',
)
def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'export-tar', '--info', 'repo::archive', 'test.tar'))
insert_logging_mock(logging.INFO)
@ -114,10 +137,14 @@ def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
)
def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--debug', '--show-rc', 'repo::archive', 'test.tar')
@ -131,10 +158,14 @@ def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
)
def test_export_tar_archive_calls_borg_with_dry_run_parameter():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module).should_receive('execute_command').never()
@ -145,10 +176,14 @@ def test_export_tar_archive_calls_borg_with_dry_run_parameter():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
)
def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--tar-filter', 'bzip2', 'repo::archive', 'test.tar')
@ -161,11 +196,15 @@ def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
tar_filter='bzip2',
)
def test_export_tar_archive_calls_borg_with_list_parameter():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'),
@ -179,11 +218,15 @@ def test_export_tar_archive_calls_borg_with_list_parameter():
paths=None,
destination_path='test.tar',
storage_config={},
files=True,
local_borg_version='1.2.3',
list_files=True,
)
def test_export_tar_archive_calls_borg_with_strip_components_parameter():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--strip-components', '5', 'repo::archive', 'test.tar')
@ -196,11 +239,15 @@ def test_export_tar_archive_calls_borg_with_strip_components_parameter():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
strip_components=5,
)
def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('server:repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').never()
insert_execute_command_mock(('borg', 'export-tar', 'server:repo::archive', 'test.tar'))
@ -211,10 +258,14 @@ def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
paths=None,
destination_path='test.tar',
storage_config={},
local_borg_version='1.2.3',
)
def test_export_tar_archive_calls_borg_with_stdout_destination_path():
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'export-tar', 'repo::archive', '-'), capture=False)
@ -225,4 +276,5 @@ def test_export_tar_archive_calls_borg_with_stdout_destination_path():
paths=None,
destination_path='-',
storage_config={},
local_borg_version='1.2.3',
)

View file

@ -23,88 +23,109 @@ def insert_execute_command_output_mock(command, result):
def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
module.extract_last_archive_dry_run(
storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
)
def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.rlist).should_receive('resolve_archive_name').and_raise(ValueError)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(('repo',))
module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
module.extract_last_archive_dry_run(
storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
)
def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
insert_execute_command_output_mock(
('borg', 'list', '--short', '--info', 'repo'), result='archive1\narchive2\n'
)
insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2'))
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
module.extract_last_archive_dry_run(
storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
)
def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
insert_execute_command_output_mock(
('borg', 'list', '--short', '--debug', '--show-rc', 'repo'), result='archive1\narchive2\n'
)
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2')
('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive')
)
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=None)
module.extract_last_archive_dry_run(
storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=None
)
def test_extract_last_archive_dry_run_calls_borg_via_local_path():
insert_execute_command_output_mock(
('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive'))
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(
storage_config={}, repository='repo', lock_wait=None, local_path='borg1'
storage_config={},
local_borg_version='1.2.3',
repository='repo',
lock_wait=None,
local_path='borg1',
)
def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
insert_execute_command_output_mock(
('borg', 'list', '--short', '--remote-path', 'borg1', 'repo'), result='archive1\narchive2\n'
)
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2')
('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive')
)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(
storage_config={}, repository='repo', lock_wait=None, remote_path='borg1'
storage_config={},
local_borg_version='1.2.3',
repository='repo',
lock_wait=None,
remote_path='borg1',
)
def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
insert_execute_command_output_mock(
('borg', 'list', '--short', '--lock-wait', '5', 'repo'), result='archive1\narchive2\n'
)
flexmock(module.rlist).should_receive('resolve_archive_name').and_return('archive')
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2')
('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive')
)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(storage_config={}, repository='repo', lock_wait=5)
module.extract_last_archive_dry_run(
storage_config={}, local_borg_version='1.2.3', repository='repo', lock_wait=5
)
def test_extract_archive_calls_borg_with_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -121,6 +142,9 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -141,13 +165,16 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(feature_available)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
location_config={'numeric_owner': True},
location_config={'numeric_ids': True},
storage_config={},
local_borg_version='1.2.3',
)
@ -157,6 +184,9 @@ def test_extract_archive_calls_borg_with_umask_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -173,6 +203,9 @@ def test_extract_archive_calls_borg_with_lock_wait_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -190,6 +223,9 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -209,6 +245,9 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
)
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -225,6 +264,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=True,
@ -241,6 +283,9 @@ def test_extract_archive_calls_borg_with_destination_path():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest')
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -258,6 +303,9 @@ def test_extract_archive_calls_borg_with_strip_components():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -281,6 +329,9 @@ def test_extract_archive_calls_borg_with_progress_parameter():
extra_environment=None,
).once()
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
module.extract_archive(
dry_run=False,
@ -323,6 +374,9 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
extra_environment=None,
).and_return(process).once()
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('repo::archive',)
)
assert (
module.extract_archive(
@ -346,6 +400,9 @@ def test_extract_archive_skips_abspath_for_remote_repository():
('borg', 'extract', 'server:repo::archive'), working_directory=None, extra_environment=None,
).once()
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
('server:repo::archive',)
)
module.extract_archive(
dry_run=False,

View file

@ -45,3 +45,34 @@ def test_make_flags_from_arguments_omits_excludes():
arguments = flexmock(foo='bar', baz='quux')
assert module.make_flags_from_arguments(arguments, excludes=('baz', 'other')) == ('foo', 'bar')
def test_make_repository_flags_with_borg_features_includes_repo_flag():
flexmock(module.feature).should_receive('available').and_return(True)
assert module.make_repository_flags(repository='repo', local_borg_version='1.2.3') == (
'--repo',
'repo',
)
def test_make_repository_flags_without_borg_features_includes_omits_flag():
flexmock(module.feature).should_receive('available').and_return(False)
assert module.make_repository_flags(repository='repo', local_borg_version='1.2.3') == ('repo',)
def test_make_repository_archive_flags_with_borg_features_separates_repository_and_archive():
flexmock(module.feature).should_receive('available').and_return(True)
assert module.make_repository_archive_flags(
repository='repo', archive='archive', local_borg_version='1.2.3'
) == ('--repo', 'repo', 'archive',)
def test_make_repository_archive_flags_with_borg_features_joins_repository_and_archive():
flexmock(module.feature).should_receive('available').and_return(False)
assert module.make_repository_archive_flags(
repository='repo', archive='archive', local_borg_version='1.2.3'
) == ('repo::archive',)

View file

@ -9,6 +9,31 @@ from ..test_verbosity import insert_logging_mock
def test_display_archives_info_calls_borg_with_parameters():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
)
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False, prefix=None),
)
def test_display_archives_info_without_borg_features_calls_borg_without_repo_flag():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', 'repo'),
@ -18,28 +43,42 @@ def test_display_archives_info_calls_borg_with_parameters():
)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False, prefix=None),
)
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--info', 'repo'),
('borg', 'info', '--info', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
)
insert_logging_mock(logging.INFO)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False, prefix=None),
)
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
@ -47,16 +86,23 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
insert_logging_mock(logging.INFO)
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True, prefix=None),
)
assert json_output == '[]'
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--debug', '--show-rc', 'repo'),
('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
@ -64,14 +110,21 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
insert_logging_mock(logging.DEBUG)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False, prefix=None),
)
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
@ -79,29 +132,67 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
insert_logging_mock(logging.DEBUG)
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True, prefix=None),
)
assert json_output == '[]'
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
flexmock(module.flags).should_receive('make_flags').and_return(())
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
).and_return('[]')
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True, prefix=None),
)
assert json_output == '[]'
def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
def test_display_archives_info_with_archive_calls_borg_with_glob_archives_pa