diff --git a/.drone.yml b/.drone.yml index a2e92ac95..cf4c358a1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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: diff --git a/NEWS b/NEWS index 7edc60e4b..274705389 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,35 @@ -1.6.6.dev0 +1.7.0.dev0 + * #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. + * 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 diff --git a/README.md b/README.md index 310d23af8..05d4d9bf4 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/borgmatic/borg/borg.py b/borgmatic/borg/borg.py index 24a88643f..991b736bf 100644 --- a/borgmatic/borg/borg.py +++ b/borgmatic/borg/borg.py @@ -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( diff --git a/borgmatic/borg/check.py b/borgmatic/borg/check.py index 83c3eea2d..e3dd0c32b 100644 --- a/borgmatic/borg/check.py +++ b/borgmatic/borg/check.py @@ -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')) diff --git a/borgmatic/borg/compact.py b/borgmatic/borg/compact.py index 17f0f1a64..847ed26b3 100644 --- a/borgmatic/borg/compact.py +++ b/borgmatic/borg/compact.py @@ -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), + ) diff --git a/borgmatic/borg/create.py b/borgmatic/borg/create.py index 1a7ae4004..9977e05a3 100644 --- a/borgmatic/borg/create.py +++ b/borgmatic/borg/create.py @@ -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__) @@ -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')) @@ -298,11 +298,7 @@ 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 ) diff --git a/borgmatic/borg/export_tar.py b/borgmatic/borg/export_tar.py index 542092880..9bbd00823 100644 --- a/borgmatic/borg/export_tar.py +++ b/borgmatic/borg/export_tar.py @@ -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,6 +14,7 @@ def export_tar_archive( paths, destination_path, storage_config, + local_borg_version, local_path='borg', remote_path=None, tar_filter=None, @@ -22,10 +23,10 @@ def export_tar_archive( ): ''' 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. ''' @@ -43,7 +44,11 @@ def export_tar_archive( + (('--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 ()) ) diff --git a/borgmatic/borg/extract.py b/borgmatic/borg/extract.py index 3f4df1350..8ea8bbbd3 100644 --- a/borgmatic/borg/extract.py +++ b/borgmatic/borg/extract.py @@ -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 ()) ) diff --git a/borgmatic/borg/feature.py b/borgmatic/borg/feature.py index af87d309e..0b77eb75b 100644 --- a/borgmatic/borg/feature.py +++ b/borgmatic/borg/feature.py @@ -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 } diff --git a/borgmatic/borg/flags.py b/borgmatic/borg/flags.py index 0665607a2..81b6a6b13 100644 --- a/borgmatic/borg/flags.py +++ b/borgmatic/borg/flags.py @@ -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}',) + ) diff --git a/borgmatic/borg/info.py b/borgmatic/borg/info.py index 6e783e88e..9e7f484e6 100644 --- a/borgmatic/borg/info.py +++ b/borgmatic/borg/info.py @@ -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 () + ) ) ) diff --git a/borgmatic/borg/init.py b/borgmatic/borg/init.py deleted file mode 100644 index 9329bc1b0..000000000 --- a/borgmatic/borg/init.py +++ /dev/null @@ -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), - ) diff --git a/borgmatic/borg/list.py b/borgmatic/borg/list.py index 1d897ec73..360f5979b 100644 --- a/borgmatic/borg/list.py +++ b/borgmatic/borg/list.py @@ -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 diff --git a/borgmatic/borg/mount.py b/borgmatic/borg/mount.py index 4f5db81a4..c1ad1c3d1 100644 --- a/borgmatic/borg/mount.py +++ b/borgmatic/borg/mount.py @@ -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 ()) ) diff --git a/borgmatic/borg/prune.py b/borgmatic/borg/prune.py index 9b2f2b44d..54c428c1d 100644 --- a/borgmatic/borg/prune.py +++ b/borgmatic/borg/prune.py @@ -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,6 +37,7 @@ def prune_archives( repository, storage_config, retention_config, + local_borg_version, local_path='borg', remote_path=None, stats=False, @@ -55,7 +54,7 @@ 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 ()) @@ -65,7 +64,7 @@ def prune_archives( + (('--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: diff --git a/borgmatic/borg/rcreate.py b/borgmatic/borg/rcreate.py new file mode 100644 index 000000000..d3a8f7aa3 --- /dev/null +++ b/borgmatic/borg/rcreate.py @@ -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), + ) diff --git a/borgmatic/borg/rinfo.py b/borgmatic/borg/rinfo.py new file mode 100644 index 000000000..3fc8bb5cf --- /dev/null +++ b/borgmatic/borg/rinfo.py @@ -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), + ) diff --git a/borgmatic/borg/rlist.py b/borgmatic/borg/rlist.py new file mode 100644 index 000000000..c2e9299db --- /dev/null +++ b/borgmatic/borg/rlist.py @@ -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 diff --git a/borgmatic/borg/transfer.py b/borgmatic/borg/transfer.py new file mode 100644 index 000000000..8647fb9b9 --- /dev/null +++ b/borgmatic/borg/transfer.py @@ -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), + ) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 46fcae4cc..e5be542bb 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -4,7 +4,7 @@ from argparse import Action, ArgumentParser from borgmatic.config import collect SUBPARSER_ALIASES = { - 'init': ['--init', '-I'], + 'rcreate': ['init', '--init', '-I'], 'prune': ['--prune', '-p'], 'compact': [], 'create': ['--create', '-C'], @@ -14,8 +14,11 @@ SUBPARSER_ALIASES = { 'mount': ['--mount', '-m'], 'umount': ['--umount', '-u'], 'restore': ['--restore', '-r'], + 'rlist': [], 'list': ['--list', '-l'], + 'rinfo': [], 'info': ['--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', @@ -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') @@ -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 diff --git a/borgmatic/commands/borgmatic.py b/borgmatic/commands/borgmatic.py index 1ec250ffe..c03fc4418 100644 --- a/borgmatic/commands/borgmatic.py +++ b/borgmatic/commands/borgmatic.py @@ -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,30 @@ def run_actions( 'repositories': ','.join(location['repositories']), } - if 'init' in arguments: - logger.info('{}: Initializing repository'.format(repository)) - borg_init.initialize_repository( + 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,6 +294,7 @@ def run_actions( repository, storage, retention, + local_borg_version, local_path=local_path, remote_path=remote_path, stats=arguments['prune'].stats, @@ -302,6 +322,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, @@ -396,6 +417,7 @@ def run_actions( location, storage, consistency, + local_borg_version, local_path=local_path, remote_path=remote_path, progress=arguments['check'].progress, @@ -429,8 +451,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,12 +489,18 @@ 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, @@ -487,14 +520,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 +559,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 +635,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,12 +727,18 @@ 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, @@ -661,9 +759,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 +867,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( diff --git a/borgmatic/config/generate.py b/borgmatic/config/generate.py index a454f662c..e864a3c0c 100644 --- a/borgmatic/config/generate.py +++ b/borgmatic/config/generate.py @@ -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 diff --git a/borgmatic/config/normalize.py b/borgmatic/config/normalize.py index cd7723a09..a43bb309b 100644 --- a/borgmatic/config/normalize.py +++ b/borgmatic/config/normalize.py @@ -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 diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index 02caf7929..2434eccd0 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -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: @@ -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 @@ -1012,6 +1012,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: | diff --git a/borgmatic/config/validate.py b/borgmatic/config/validate.py index a782d7bbe..76f2a0f88 100644 --- a/borgmatic/config/validate.py +++ b/borgmatic/config/validate.py @@ -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. ''' @@ -99,7 +102,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv raise Validation_error(config_filename, (str(error),)) override.apply_overrides(config, overrides) - normalize.normalize(config) + 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' + ) diff --git a/borgmatic/execute.py b/borgmatic/execute.py index 8760703ed..a9874bf32 100644 --- a/borgmatic/execute.py +++ b/borgmatic/execute.py @@ -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) diff --git a/borgmatic/hooks/healthchecks.py b/borgmatic/hooks/healthchecks.py index c801f1824..03d012a8f 100644 --- a/borgmatic/hooks/healthchecks.py +++ b/borgmatic/hooks/healthchecks.py @@ -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: diff --git a/docs/Dockerfile b/docs/Dockerfile index 89cb1862d..35d50b77e 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -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 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 diff --git a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md index 1ce53bae4..689284baa 100644 --- a/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md +++ b/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server.md @@ -76,7 +76,7 @@ location: - /home repositories: - - me@buddys-server.org:backup.borg + - ssh://me@buddys-server.org/./backup.borg hooks: before_backup: diff --git a/docs/how-to/backup-your-databases.md b/docs/how-to/backup-your-databases.md index 01a84f963..6a485289a 100644 --- a/docs/how-to/backup-your-databases.md +++ b/docs/how-to/backup-your-databases.md @@ -133,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: diff --git a/docs/how-to/deal-with-very-large-backups.md b/docs/how-to/deal-with-very-large-backups.md index aa67f1f63..8318ebc42 100644 --- a/docs/how-to/deal-with-very-large-backups.md +++ b/docs/how-to/deal-with-very-large-backups.md @@ -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 diff --git a/docs/how-to/extract-a-backup.md b/docs/how-to/extract-a-backup.md index 0dd47b0cc..62bdc75b1 100644 --- a/docs/how-to/extract-a-backup.md +++ b/docs/how-to/extract-a-backup.md @@ -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 diff --git a/docs/how-to/inspect-your-backups.md b/docs/how-to/inspect-your-backups.md index 734fc3680..257e98c85 100644 --- a/docs/how-to/inspect-your-backups.md +++ b/docs/how-to/inspect-your-backups.md @@ -37,20 +37,35 @@ 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 ``` -You can change the output format of `borgmatic list` by specifying your own using `--format $FORMAT`. Refer to the official [borg list --format specs]( https://borgbackup.readthedocs.io/en/stable/usage/list.html#the-format-specifier-syntax) for available options. +You can change the output format of `borgmatic list` by specifying your own +with `--format $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!)* + +New in borgmatic version 1.7.0 +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. -*(No borgmatic `list` or `info` actions? Try the old-style `--list` or -`--info`. Or upgrade borgmatic!)* ### Searching for a file diff --git a/docs/how-to/make-backups-redundant.md b/docs/how-to/make-backups-redundant.md index c42e29ac9..5ad13ee0c 100644 --- a/docs/how-to/make-backups-redundant.md +++ b/docs/how-to/make-backups-redundant.md @@ -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 ``` diff --git a/docs/how-to/monitor-your-backups.md b/docs/how-to/monitor-your-backups.md index 3dabb7de2..7e62dac7a 100644 --- a/docs/how-to/monitor-your-backups.md +++ b/docs/how-to/monitor-your-backups.md @@ -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 diff --git a/docs/how-to/run-arbitrary-borg-commands.md b/docs/how-to/run-arbitrary-borg-commands.md index f0e152947..8ffddebf7 100644 --- a/docs/how-to/run-arbitrary-borg-commands.md +++ b/docs/how-to/run-arbitrary-borg-commands.md @@ -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. +However, 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 diff --git a/docs/how-to/set-up-backups.md b/docs/how-to/set-up-backups.md index 4206f6cab..142ea62ef 100644 --- a/docs/how-to/set-up-backups.md +++ b/docs/how-to/set-up-backups.md @@ -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!) +New in borgmatic version 1.7.0 +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,8 +228,8 @@ 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 @@ -230,7 +237,7 @@ sudo borgmatic create --verbosity 1 --files --stats ``` (No borgmatic `--files` flag? It's only present in newer versions of -borgmatic. So try leaving it out, or upgrade borgmatic!) +borgmatic. So try leaving 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. diff --git a/docs/how-to/upgrade.md b/docs/how-to/upgrade.md index cdcdefe5f..28955c49d 100644 --- a/docs/how-to/upgrade.md +++ b/docs/how-to/upgrade.md @@ -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,85 @@ 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). However, some +more major Borg releases require additional steps that borgmatic can help +with. + + +### Borg 1.2 to 2.0 + +New in borgmatic version 1.7.0 +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. diff --git a/scripts/run-full-tests b/scripts/run-full-tests index a54e0abdd..993c5bd89 100755 --- a/scripts/run-full-tests +++ b/scripts/run-full-tests @@ -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 diff --git a/setup.py b/setup.py index 9cf64a16b..20d101ac7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = '1.6.6.dev0' +VERSION = '1.7.0.dev0' setup( diff --git a/tests/end-to-end/test_generate_config.py b/tests/end-to-end/test_generate_config.py new file mode 100644 index 000000000..b8cade96d --- /dev/null +++ b/tests/end-to-end/test_generate_config.py @@ -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( + ' ' + ) + ) diff --git a/tests/integration/commands/test_arguments.py b/tests/integration/commands/test_arguments.py index e13a31b6b..26c569833 100644 --- a/tests/integration/commands/test_arguments.py +++ b/tests/integration/commands/test_arguments.py @@ -287,15 +287,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']) @@ -496,6 +487,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']) diff --git a/tests/integration/config/test_validate.py b/tests/integration/config/test_validate.py index 2cd164303..5d948ae2d 100644 --- a/tests/integration/config/test_validate.py +++ b/tests/integration/config/test_validate.py @@ -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 == [] diff --git a/tests/integration/test_execute.py b/tests/integration/test_execute.py index 1647a9e09..bb618eccc 100644 --- a/tests/integration/test_execute.py +++ b/tests/integration/test_execute.py @@ -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) diff --git a/tests/unit/borg/test_borg.py b/tests/unit/borg/test_borg.py index 8282bd527..96e591566 100644 --- a/tests/unit/borg/test_borg.py +++ b/tests/unit/borg/test_borg.py @@ -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'], ) diff --git a/tests/unit/borg/test_check.py b/tests/unit/borg/test_check.py index 0b4c0cef5..60bfcefbc 100644 --- a/tests/unit/borg/test_check.py +++ b/tests/unit/borg/test_check.py @@ -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', ) diff --git a/tests/unit/borg/test_compact.py b/tests/unit/borg/test_compact.py index 4c2abb7a0..36760f3c3 100644 --- a/tests/unit/borg/test_compact.py +++ b/tests/unit/borg/test_compact.py @@ -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', ) diff --git a/tests/unit/borg/test_create.py b/tests/unit/borg/test_create.py index c51bd34fa..453dbf427 100644 --- a/tests/unit/borg/test_create.py +++ b/tests/unit/borg/test_create.py @@ -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', @@ -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', @@ -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, diff --git a/tests/unit/borg/test_export_tar.py b/tests/unit/borg/test_export_tar.py index 11b2b20af..f8d56372b 100644 --- a/tests/unit/borg/test_export_tar.py +++ b/tests/unit/borg/test_export_tar.py @@ -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={}, + local_borg_version='1.2.3', 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', ) diff --git a/tests/unit/borg/test_extract.py b/tests/unit/borg/test_extract.py index 58503f7cc..d572ff9e4 100644 --- a/tests/unit/borg/test_extract.py +++ b/tests/unit/borg/test_extract.py @@ -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, diff --git a/tests/unit/borg/test_flags.py b/tests/unit/borg/test_flags.py index 3d1153ec3..7bf621dac 100644 --- a/tests/unit/borg/test_flags.py +++ b/tests/unit/borg/test_flags.py @@ -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',) diff --git a/tests/unit/borg/test_info.py b/tests/unit/borg/test_info.py index b91f56094..a6689ac5a 100644 --- a/tests/unit/borg/test_info.py +++ b/tests/unit/borg/test_info.py @@ -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_parameter(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'archive' + ).and_return(('--glob-archives', 'archive')) + 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', '--glob-archives', 'archive'), + 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='archive', json=False, prefix=None), + ) + + +def test_display_archives_info_with_archive_and_without_borg_features_calls_borg_with_repo_archive_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::archive',)) + 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::archive'), @@ -111,14 +202,21 @@ def test_display_archives_info_with_archive_calls_borg_with_archive_parameter(): ) module.display_archives_info( - repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False) + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + info_arguments=flexmock(archive='archive', json=False, prefix=None), ) def test_display_archives_info_with_local_path_calls_borg_via_local_path(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg1', 'info', 'repo'), + ('borg1', 'info', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1', extra_environment=None, @@ -127,15 +225,23 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path(): module.display_archives_info( repository='repo', storage_config={}, - info_arguments=flexmock(archive=None, json=False), + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False, prefix=None), local_path='borg1', ) def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'remote-path', 'borg1' + ).and_return(('--remote-path', 'borg1')) + 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', '--remote-path', 'borg1', 'repo'), + ('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -144,16 +250,24 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para module.display_archives_info( repository='repo', storage_config={}, - info_arguments=flexmock(archive=None, json=False), + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False, prefix=None), remote_path='borg1', ) def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) storage_config = {'lock_wait': 5} + 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', '--lock-wait', '5', 'repo'), + ('borg', 'info', '--lock-wait', '5', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -162,15 +276,22 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete module.display_archives_info( repository='repo', storage_config=storage_config, - info_arguments=flexmock(archive=None, json=False), + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False, prefix=None), ) -@pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last')) -def test_display_archives_info_passes_through_arguments_to_borg(argument_name): +def test_display_archives_info_with_prefix_calls_borg_with_glob_archives_parameters(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'foo*' + ).and_return(('--glob-archives', 'foo*')) + 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', '--' + argument_name.replace('_', '-'), 'value', 'repo'), + ('borg', 'info', '--glob-archives', 'foo*', '--repo', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, @@ -179,5 +300,31 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name): module.display_archives_info( repository='repo', storage_config={}, - info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}), + local_borg_version='2.3.4', + info_arguments=flexmock(archive=None, json=False, prefix='foo'), + ) + + +@pytest.mark.parametrize('argument_name', ('glob_archives', 'sort_by', 'first', 'last')) +def test_display_archives_info_passes_through_arguments_to_borg(argument_name): + flag_name = f"--{argument_name.replace('_', ' ')}" + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (flag_name, 'value') + ) + 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', flag_name, 'value', '--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, **{argument_name: 'value'}), ) diff --git a/tests/unit/borg/test_init.py b/tests/unit/borg/test_init.py deleted file mode 100644 index fb81f344c..000000000 --- a/tests/unit/borg/test_init.py +++ /dev/null @@ -1,132 +0,0 @@ -import logging -import subprocess - -import pytest -from flexmock import flexmock - -from borgmatic.borg import init as module - -from ..test_verbosity import insert_logging_mock - -INFO_SOME_UNKNOWN_EXIT_CODE = -999 -INIT_COMMAND = ('borg', 'init', '--encryption', 'repokey') - - -def insert_info_command_found_mock(): - flexmock(module.info).should_receive('display_archives_info') - - -def insert_info_command_not_found_mock(): - flexmock(module.info).should_receive('display_archives_info').and_raise( - subprocess.CalledProcessError(module.INFO_REPOSITORY_NOT_FOUND_EXIT_CODE, []) - ) - - -def insert_init_command_mock(init_command, **kwargs): - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - init_command, - output_file=module.DO_NOT_CAPTURE, - borg_local_path=init_command[0], - extra_environment=None, - ).once() - - -def test_initialize_repository_calls_borg_with_parameters(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('repo',)) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_raises_for_borg_init_error(): - insert_info_command_not_found_mock() - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').and_raise( - module.subprocess.CalledProcessError(2, 'borg init') - ) - - with pytest.raises(subprocess.CalledProcessError): - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey' - ) - - -def test_initialize_repository_skips_initialization_when_repository_already_exists(): - insert_info_command_found_mock() - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_raises_for_unknown_info_command_error(): - flexmock(module.info).should_receive('display_archives_info').and_raise( - subprocess.CalledProcessError(INFO_SOME_UNKNOWN_EXIT_CODE, []) - ) - - with pytest.raises(subprocess.CalledProcessError): - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey' - ) - - -def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--append-only', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', append_only=True - ) - - -def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', storage_quota='5G' - ) - - -def test_initialize_repository_with_log_info_calls_borg_with_info_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--info', 'repo')) - insert_logging_mock(logging.INFO) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo')) - insert_logging_mock(logging.DEBUG) - - module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey') - - -def test_initialize_repository_with_local_path_calls_borg_via_local_path(): - insert_info_command_not_found_mock() - insert_init_command_mock(('borg1',) + INIT_COMMAND[1:] + ('repo',)) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', local_path='borg1' - ) - - -def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1', 'repo')) - - module.initialize_repository( - repository='repo', storage_config={}, encryption_mode='repokey', remote_path='borg1' - ) - - -def test_initialize_repository_with_extra_borg_options_calls_borg_with_extra_options(): - insert_info_command_not_found_mock() - insert_init_command_mock(INIT_COMMAND + ('--extra', '--options', 'repo')) - - module.initialize_repository( - repository='repo', - storage_config={'extra_borg_options': {'init': '--extra --options'}}, - encryption_mode='repokey', - ) diff --git a/tests/unit/borg/test_list.py b/tests/unit/borg/test_list.py index 6123a077b..0019d9798 100644 --- a/tests/unit/borg/test_list.py +++ b/tests/unit/borg/test_list.py @@ -8,129 +8,17 @@ from borgmatic.borg import list as module from ..test_verbosity import insert_logging_mock -BORG_LIST_LATEST_ARGUMENTS = ( - '--last', - '1', - '--short', - 'repo', -) - - -def test_resolve_archive_name_passes_through_non_latest_archive_name(): - archive = 'myhost-2030-01-01T14:41:17.647620' - - assert module.resolve_archive_name('repo', archive, storage_config={}) == archive - - -def test_resolve_archive_name_calls_borg_with_parameters(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - insert_logging_mock(logging.INFO) - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - insert_logging_mock(logging.DEBUG) - - assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive - - -def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg1', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1') - == expected_archive - ) - - -def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters(): - expected_archive = 'archive-name' - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1') - == expected_archive - ) - - -def test_resolve_archive_name_without_archives_raises(): - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return('') - - with pytest.raises(ValueError): - module.resolve_archive_name('repo', 'latest', storage_config={}) - - -def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters(): - expected_archive = 'archive-name' - - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS, - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).and_return(expected_archive + '\n') - - assert ( - module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'}) - == expected_archive - ) - def test_make_list_command_includes_log_info(): insert_logging_mock(logging.INFO) + 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',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -139,10 +27,14 @@ def test_make_list_command_includes_log_info(): def test_make_list_command_includes_json_but_not_info(): insert_logging_mock(logging.INFO) + 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',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -151,10 +43,14 @@ def test_make_list_command_includes_json_but_not_info(): def test_make_list_command_includes_log_debug(): insert_logging_mock(logging.DEBUG) + 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',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -163,10 +59,14 @@ def test_make_list_command_includes_log_debug(): def test_make_list_command_includes_json_but_not_debug(): insert_logging_mock(logging.DEBUG) + 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',)) command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -174,9 +74,14 @@ def test_make_list_command_includes_json_but_not_debug(): def test_make_list_command_includes_json(): + 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',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=True), ) @@ -184,9 +89,16 @@ def test_make_list_command_includes_json(): def test_make_list_command_includes_lock_wait(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={'lock_wait': 5}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), ) @@ -194,9 +106,16 @@ def test_make_list_command_includes_lock_wait(): def test_make_list_command_includes_archive(): + 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_archive_flags').and_return( + ('repo::archive',) + ) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive='archive', paths=None, json=False), ) @@ -204,9 +123,16 @@ def test_make_list_command_includes_archive(): def test_make_list_command_includes_archive_and_path(): + 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_archive_flags').and_return( + ('repo::archive',) + ) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False), ) @@ -214,9 +140,14 @@ def test_make_list_command_includes_archive_and_path(): def test_make_list_command_includes_local_path(): + 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',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), local_path='borg2', ) @@ -225,9 +156,16 @@ def test_make_list_command_includes_local_path(): def test_make_list_command_includes_remote_path(): + flexmock(module.flags).should_receive('make_flags').and_return( + ('--remote-path', 'borg2') + ).and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False), remote_path='borg2', ) @@ -236,9 +174,14 @@ def test_make_list_command_includes_remote_path(): def test_make_list_command_includes_short(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock(archive=None, paths=None, json=False, short=True), ) @@ -260,16 +203,23 @@ def test_make_list_command_includes_short(): ), ) def test_make_list_command_includes_additional_flags(argument_name): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (f"--{argument_name.replace('_', '-')}", 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + command = module.make_list_command( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=flexmock( archive=None, paths=None, json=False, find_paths=None, format=None, - **{argument_name: 'value'} + **{argument_name: 'value'}, ), ) @@ -303,97 +253,124 @@ def test_make_find_paths_adds_globs_to_path_fragments(): assert module.make_find_paths(('foo.txt',)) == ('sh:**/*foo.txt*/**',) -def test_list_archives_calls_borg_with_parameters(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None) +def test_list_archive_calls_borg_with_parameters(): + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg', remote_path=None, - ).and_return(('borg', 'list', 'repo')) + ).and_return(('borg', 'list', 'repo::archive')) flexmock(module).should_receive('make_find_paths').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', 'repo'), + ('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg', extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, - ) - - -def test_list_archives_with_json_suppresses_most_borg_output(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=True, find_paths=None) - - flexmock(module).should_receive('make_list_command').with_args( + module.list_archive( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, - local_path='borg', - remote_path=None, - ).and_return(('borg', 'list', 'repo')) - flexmock(module).should_receive('make_find_paths').and_return(()) - flexmock(module.environment).should_receive('make_environment') - flexmock(module).should_receive('execute_command').with_args( - ('borg', 'list', 'repo'), - output_log_level=None, - borg_local_path='borg', - extra_environment=None, - ).once() - - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, ) -def test_list_archives_calls_borg_with_local_path(): - list_arguments = argparse.Namespace(archive=None, paths=None, json=False, find_paths=None) +def test_list_archive_with_archive_and_json_errors(): + list_arguments = argparse.Namespace(archive='archive', paths=None, json=True, find_paths=None) + flexmock(module.feature).should_receive('available').and_return(False) + + with pytest.raises(ValueError): + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +def test_list_archive_calls_borg_with_local_path(): + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) + + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg2', remote_path=None, - ).and_return(('borg2', 'list', 'repo')) + ).and_return(('borg2', 'list', 'repo::archive')) flexmock(module).should_receive('make_find_paths').and_return(()) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( - ('borg2', 'list', 'repo'), + ('borg2', 'list', 'repo::archive'), output_log_level=logging.WARNING, borg_local_path='borg2', extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, local_path='borg2', + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + local_path='borg2', ) -def test_list_archives_calls_borg_multiple_times_with_find_paths(): +def test_list_archive_calls_borg_multiple_times_with_find_paths(): glob_paths = ('**/*foo.txt*/**',) list_arguments = argparse.Namespace( - archive=None, paths=None, json=False, find_paths=['foo.txt'], format=None + archive=None, + json=False, + find_paths=['foo.txt'], + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, ) - flexmock(module).should_receive('make_list_command').and_return( - ('borg', 'list', 'repo') - ).and_return(('borg', 'list', 'repo::archive1')).and_return(('borg', 'list', 'repo::archive2')) - flexmock(module).should_receive('make_find_paths').and_return(glob_paths) - flexmock(module.environment).should_receive('make_environment') + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.rlist).should_receive('make_rlist_command').and_return(('borg', 'list', 'repo')) flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo'), output_log_level=None, borg_local_path='borg', extra_environment=None, - ).and_return( - 'archive1 Sun, 2022-05-29 15:27:04 [abc]\narchive2 Mon, 2022-05-30 19:47:15 [xyz]' - ).once() + ).and_return('archive1\narchive2').once() + flexmock(module).should_receive('make_list_command').and_return( + ('borg', 'list', 'repo::archive1') + ).and_return(('borg', 'list', 'repo::archive2')) + flexmock(module).should_receive('make_find_paths').and_return(glob_paths) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'list', 'repo::archive1') + glob_paths, @@ -408,17 +385,32 @@ def test_list_archives_calls_borg_multiple_times_with_find_paths(): extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, ) -def test_list_archives_calls_borg_with_archive(): - list_arguments = argparse.Namespace(archive='archive', paths=None, json=False, find_paths=None) +def test_list_archive_calls_borg_with_archive(): + list_arguments = argparse.Namespace( + archive='archive', + paths=None, + json=False, + find_paths=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + ) + flexmock(module.feature).should_receive('available').and_return(False) flexmock(module).should_receive('make_list_command').with_args( repository='repo', storage_config={}, + local_borg_version='1.2.3', list_arguments=list_arguments, local_path='borg', remote_path=None, @@ -432,6 +424,211 @@ def test_list_archives_calls_borg_with_archive(): extra_environment=None, ).once() - module.list_archives( - repository='repo', storage_config={}, list_arguments=list_arguments, + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +def test_list_archive_without_archive_delegates_to_list_repository(): + list_arguments = argparse.Namespace( + archive=None, + short=None, + format=None, + json=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + find_paths=None, + ) + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.rlist).should_receive('list_repository') + flexmock(module.environment).should_receive('make_environment').never() + flexmock(module).should_receive('execute_command').never() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +def test_list_archive_with_borg_features_without_archive_delegates_to_list_repository(): + list_arguments = argparse.Namespace( + archive=None, + short=None, + format=None, + json=None, + prefix=None, + glob_archives=None, + sort_by=None, + first=None, + last=None, + find_paths=None, + ) + + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.rlist).should_receive('list_repository') + flexmock(module.environment).should_receive('make_environment').never() + flexmock(module).should_receive('execute_command').never() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=list_arguments, + ) + + +@pytest.mark.parametrize( + 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), +) +def test_list_archive_with_archive_ignores_archive_filter_flag(archive_filter_flag,): + default_filter_flags = { + 'prefix': None, + 'glob_archives': None, + 'sort_by': None, + 'first': None, + 'last': None, + } + altered_filter_flags = {**default_filter_flags, **{archive_filter_flag: 'foo'}} + + flexmock(module.feature).should_receive('available').with_args( + module.feature.Feature.RLIST, '1.2.3' + ).and_return(False) + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **default_filter_flags + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', 'repo::archive')) + flexmock(module).should_receive('make_find_paths').and_return(()) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', 'repo::archive'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + archive='archive', paths=None, json=False, find_paths=None, **altered_filter_flags + ), + ) + + +@pytest.mark.parametrize( + 'archive_filter_flag', ('prefix', 'glob_archives', 'sort_by', 'first', 'last',), +) +def test_list_archive_with_find_paths_allows_archive_filter_flag_but_only_passes_it_to_rlist( + archive_filter_flag, +): + default_filter_flags = { + 'prefix': None, + 'glob_archives': None, + 'sort_by': None, + 'first': None, + 'last': None, + } + altered_filter_flags = {**default_filter_flags, **{archive_filter_flag: 'foo'}} + glob_paths = ('**/*foo.txt*/**',) + flexmock(module.feature).should_receive('available').and_return(True) + + flexmock(module.rlist).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=argparse.Namespace( + repository='repo', short=True, format=None, json=None, **altered_filter_flags + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', '--repo', 'repo')) + + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rlist', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('archive1\narchive2').once() + + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive='archive1', + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **default_filter_flags, + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', '--repo', 'repo', 'archive1')) + + flexmock(module).should_receive('make_list_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive='archive2', + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **default_filter_flags, + ), + local_path='borg', + remote_path=None, + ).and_return(('borg', 'list', '--repo', 'repo', 'archive2')) + + flexmock(module).should_receive('make_find_paths').and_return(glob_paths) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--repo', 'repo', 'archive1') + glob_paths, + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--repo', 'repo', 'archive2') + glob_paths, + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_archive( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + list_arguments=argparse.Namespace( + repository='repo', + archive=None, + paths=None, + short=True, + format=None, + json=None, + find_paths=['foo.txt'], + **altered_filter_flags, + ), ) diff --git a/tests/unit/borg/test_mount.py b/tests/unit/borg/test_mount.py index c5638caa9..98bbe1a67 100644 --- a/tests/unit/borg/test_mount.py +++ b/tests/unit/borg/test_mount.py @@ -14,7 +14,47 @@ def insert_execute_command_mock(command): ).once() -def test_mount_archive_calls_borg_with_required_parameters(): +def test_mount_archive_calls_borg_with_required_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + insert_execute_command_mock(('borg', 'mount', 'repo', '/mnt')) + + module.mount_archive( + repository='repo', + archive=None, + mount_point='/mnt', + paths=None, + foreground=False, + options=None, + storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_mount_archive_with_borg_features_calls_borg_with_repository_and_glob_archives_flags(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + insert_execute_command_mock( + ('borg', 'mount', '--repo', 'repo', '--glob-archives', 'archive', '/mnt') + ) + + module.mount_archive( + repository='repo', + archive='archive', + mount_point='/mnt', + paths=None, + foreground=False, + options=None, + storage_config={}, + local_borg_version='1.2.3', + ) + + +def test_mount_archive_without_archive_calls_borg_with_repository_flags_only(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt')) module.mount_archive( @@ -25,10 +65,15 @@ def test_mount_archive_calls_borg_with_required_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) -def test_mount_archive_calls_borg_with_path_parameters(): +def test_mount_archive_calls_borg_with_path_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt', 'path1', 'path2')) module.mount_archive( @@ -39,10 +84,15 @@ def test_mount_archive_calls_borg_with_path_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) -def test_mount_archive_calls_borg_with_remote_path_parameters(): +def test_mount_archive_calls_borg_with_remote_path_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock( ('borg', 'mount', '--remote-path', 'borg1', 'repo::archive', '/mnt') ) @@ -55,11 +105,16 @@ def test_mount_archive_calls_borg_with_remote_path_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', remote_path='borg1', ) -def test_mount_archive_calls_borg_with_umask_parameters(): +def test_mount_archive_calls_borg_with_umask_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--umask', '0770', 'repo::archive', '/mnt')) module.mount_archive( @@ -70,10 +125,15 @@ def test_mount_archive_calls_borg_with_umask_parameters(): foreground=False, options=None, storage_config={'umask': '0770'}, + local_borg_version='1.2.3', ) -def test_mount_archive_calls_borg_with_lock_wait_parameters(): +def test_mount_archive_calls_borg_with_lock_wait_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--lock-wait', '5', 'repo::archive', '/mnt')) module.mount_archive( @@ -84,10 +144,15 @@ def test_mount_archive_calls_borg_with_lock_wait_parameters(): foreground=False, options=None, storage_config={'lock_wait': '5'}, + local_borg_version='1.2.3', ) def test_mount_archive_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--info', 'repo::archive', '/mnt')) insert_logging_mock(logging.INFO) @@ -99,10 +164,15 @@ def test_mount_archive_with_log_info_calls_borg_with_info_parameter(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) -def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters(): +def test_mount_archive_with_log_debug_calls_borg_with_debug_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '--debug', '--show-rc', 'repo::archive', '/mnt')) insert_logging_mock(logging.DEBUG) @@ -114,10 +184,15 @@ def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters(): foreground=False, options=None, storage_config={}, + local_borg_version='1.2.3', ) def test_mount_archive_calls_borg_with_foreground_parameter(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) flexmock(module.environment).should_receive('make_environment') flexmock(module).should_receive('execute_command').with_args( ('borg', 'mount', '--foreground', 'repo::archive', '/mnt'), @@ -134,10 +209,15 @@ def test_mount_archive_calls_borg_with_foreground_parameter(): foreground=True, options=None, storage_config={}, + local_borg_version='1.2.3', ) -def test_mount_archive_calls_borg_with_options_parameters(): +def test_mount_archive_calls_borg_with_options_flags(): + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module.flags).should_receive('make_repository_archive_flags').and_return( + ('repo::archive',) + ) insert_execute_command_mock(('borg', 'mount', '-o', 'super_mount', 'repo::archive', '/mnt')) module.mount_archive( @@ -148,4 +228,5 @@ def test_mount_archive_calls_borg_with_options_parameters(): foreground=False, options='super_mount', storage_config={}, + local_borg_version='1.2.3', ) diff --git a/tests/unit/borg/test_prune.py b/tests/unit/borg/test_prune.py index f4f34edd2..e57ce7fce 100644 --- a/tests/unit/borg/test_prune.py +++ b/tests/unit/borg/test_prune.py @@ -21,20 +21,20 @@ def insert_execute_command_mock(prune_command, output_log_level): BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3')) -def test_make_prune_flags_returns_flags_from_config_plus_default_prefix(): +def test_make_prune_flags_returns_flags_from_config_plus_default_prefix_glob(): retention_config = OrderedDict((('keep_daily', 1), ('keep_weekly', 2), ('keep_monthly', 3))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) - assert tuple(result) == BASE_PRUNE_FLAGS + (('--prefix', '{hostname}-'),) + assert tuple(result) == BASE_PRUNE_FLAGS + (('--glob-archives', '{hostname}-*'),) def test_make_prune_flags_accepts_prefix_with_placeholders(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) - expected = (('--keep-daily', '1'), ('--prefix', 'Documents_{hostname}-{now}')) + expected = (('--keep-daily', '1'), ('--glob-archives', 'Documents_{hostname}-{now}*')) assert tuple(result) == expected @@ -42,7 +42,7 @@ def test_make_prune_flags_accepts_prefix_with_placeholders(): def test_make_prune_flags_treats_empty_prefix_as_no_prefix(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', ''))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) expected = (('--keep-daily', '1'),) @@ -52,7 +52,7 @@ def test_make_prune_flags_treats_empty_prefix_as_no_prefix(): def test_make_prune_flags_treats_none_prefix_as_no_prefix(): retention_config = OrderedDict((('keep_daily', 1), ('prefix', None))) - result = module._make_prune_flags(retention_config) + result = module.make_prune_flags(retention_config) expected = (('--keep-daily', '1'),) @@ -64,59 +64,80 @@ PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '-- def test_prune_archives_calls_borg_with_parameters(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO) module.prune_archives( - dry_run=False, repository='repo', storage_config={}, retention_config=retention_config + dry_run=False, + repository='repo', + storage_config={}, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_log_info_calls_borg_with_info_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO) insert_logging_mock(logging.INFO) module.prune_archives( - repository='repo', storage_config={}, dry_run=False, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=False, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO) insert_logging_mock(logging.DEBUG) module.prune_archives( - repository='repo', storage_config={}, dry_run=False, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=False, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO) module.prune_archives( - repository='repo', storage_config={}, dry_run=True, retention_config=retention_config + repository='repo', + storage_config={}, + dry_run=True, + retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_local_path_calls_borg_via_local_path(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO) module.prune_archives( @@ -124,15 +145,17 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path(): repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', local_path='borg1', ) def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO) module.prune_archives( @@ -140,15 +163,17 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters( repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', remote_path='borg1', ) def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING) module.prune_archives( @@ -156,15 +181,17 @@ def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_o repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', stats=True, ) def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO) @@ -173,15 +200,17 @@ def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_ repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', stats=True, ) def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), logging.WARNING) module.prune_archives( @@ -189,15 +218,17 @@ def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_ou repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', files=True, ) def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_logging_mock(logging.INFO) insert_execute_command_mock(PRUNE_COMMAND + ('--info', '--list', 'repo'), logging.INFO) @@ -206,6 +237,7 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a repository='repo', storage_config={}, retention_config=retention_config, + local_borg_version='1.2.3', files=True, ) @@ -213,9 +245,10 @@ def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_a def test_prune_archives_with_umask_calls_borg_with_umask_parameters(): storage_config = {'umask': '077'} retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO) module.prune_archives( @@ -223,15 +256,17 @@ def test_prune_archives_with_umask_calls_borg_with_umask_parameters(): repository='repo', storage_config=storage_config, retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): storage_config = {'lock_wait': 5} retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO) module.prune_archives( @@ -239,14 +274,16 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters(): repository='repo', storage_config=storage_config, retention_config=retention_config, + local_borg_version='1.2.3', ) def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options(): retention_config = flexmock() - flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return( + flexmock(module).should_receive('make_prune_flags').with_args(retention_config).and_return( BASE_PRUNE_FLAGS ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO) module.prune_archives( @@ -254,4 +291,5 @@ def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options(): repository='repo', storage_config={'extra_borg_options': {'prune': '--extra --options'}}, retention_config=retention_config, + local_borg_version='1.2.3', ) diff --git a/tests/unit/borg/test_rcreate.py b/tests/unit/borg/test_rcreate.py new file mode 100644 index 000000000..612ec11c1 --- /dev/null +++ b/tests/unit/borg/test_rcreate.py @@ -0,0 +1,269 @@ +import logging +import subprocess + +import pytest +from flexmock import flexmock + +from borgmatic.borg import rcreate as module + +from ..test_verbosity import insert_logging_mock + +RINFO_SOME_UNKNOWN_EXIT_CODE = -999 +RCREATE_COMMAND = ('borg', 'rcreate', '--encryption', 'repokey') + + +def insert_rinfo_command_found_mock(): + flexmock(module.rinfo).should_receive('display_repository_info') + + +def insert_rinfo_command_not_found_mock(): + flexmock(module.rinfo).should_receive('display_repository_info').and_raise( + subprocess.CalledProcessError(module.RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE, []) + ) + + +def insert_rcreate_command_mock(rcreate_command, **kwargs): + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + rcreate_command, + output_file=module.DO_NOT_CAPTURE, + borg_local_path=rcreate_command[0], + extra_environment=None, + ).once() + + +def test_create_repository_calls_borg_with_flags(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_dry_run_skips_borg_call(): + insert_rinfo_command_not_found_mock() + flexmock(module).should_receive('execute_command').never() + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=True, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_raises_for_borg_rcreate_error(): + insert_rinfo_command_not_found_mock() + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').and_raise( + module.subprocess.CalledProcessError(2, 'borg rcreate') + ) + + with pytest.raises(subprocess.CalledProcessError): + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_skips_creation_when_repository_already_exists(): + insert_rinfo_command_found_mock() + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_raises_for_unknown_rinfo_command_error(): + flexmock(module.rinfo).should_receive('display_repository_info').and_raise( + subprocess.CalledProcessError(RINFO_SOME_UNKNOWN_EXIT_CODE, []) + ) + + with pytest.raises(subprocess.CalledProcessError): + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_source_repository_calls_borg_with_other_repo_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--other-repo', 'other.borg', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + source_repository='other.borg', + ) + + +def test_create_repository_with_copy_crypt_key_calls_borg_with_copy_crypt_key_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--copy-crypt-key', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + copy_crypt_key=True, + ) + + +def test_create_repository_with_append_only_calls_borg_with_append_only_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + append_only=True, + ) + + +def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--storage-quota', '5G', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + storage_quota='5G', + ) + + +def test_create_repository_with_make_parent_dirs_calls_borg_with_make_parent_dirs_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--make-parent-dirs', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + make_parent_dirs=True, + ) + + +def test_create_repository_with_log_info_calls_borg_with_info_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--info', '--repo', 'repo')) + insert_logging_mock(logging.INFO) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_log_debug_calls_borg_with_debug_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--debug', '--repo', 'repo')) + insert_logging_mock(logging.DEBUG) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) + + +def test_create_repository_with_local_path_calls_borg_via_local_path(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(('borg1',) + RCREATE_COMMAND[1:] + ('--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + local_path='borg1', + ) + + +def test_create_repository_with_remote_path_calls_borg_with_remote_path_flag(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + encryption_mode='repokey', + remote_path='borg1', + ) + + +def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options(): + insert_rinfo_command_not_found_mock() + insert_rcreate_command_mock(RCREATE_COMMAND + ('--extra', '--options', '--repo', 'repo')) + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + + module.create_repository( + dry_run=False, + repository='repo', + storage_config={'extra_borg_options': {'rcreate': '--extra --options'}}, + local_borg_version='2.3.4', + encryption_mode='repokey', + ) diff --git a/tests/unit/borg/test_rinfo.py b/tests/unit/borg/test_rinfo.py new file mode 100644 index 000000000..b3147b93d --- /dev/null +++ b/tests/unit/borg/test_rinfo.py @@ -0,0 +1,209 @@ +import logging + +from flexmock import flexmock + +from borgmatic.borg import rinfo as module + +from ..test_verbosity import insert_logging_mock + + +def test_display_repository_info_calls_borg_with_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_command(): + flexmock(module.feature).should_receive('available').and_return(False) + 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', 'info', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_info_calls_borg_with_info_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--info', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.INFO) + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + insert_logging_mock(logging.INFO) + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_log_debug_calls_borg_with_debug_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.DEBUG) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) + + +def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_output(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + insert_logging_mock(logging.DEBUG) + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_json_calls_borg_with_json_parameter(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--json', '--repo', 'repo'), + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('[]') + + json_output = module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=True), + ) + + assert json_output == '[]' + + +def test_display_repository_info_with_local_path_calls_borg_via_local_path(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg1', 'rinfo', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg1', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + local_path='borg1', + ) + + +def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_parameters(): + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + remote_path='borg1', + ) + + +def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_parameters(): + storage_config = {'lock_wait': 5} + flexmock(module.feature).should_receive('available').and_return(True) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo',)) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.display_repository_info( + repository='repo', + storage_config=storage_config, + local_borg_version='2.3.4', + rinfo_arguments=flexmock(json=False), + ) diff --git a/tests/unit/borg/test_rlist.py b/tests/unit/borg/test_rlist.py new file mode 100644 index 000000000..d16b53db7 --- /dev/null +++ b/tests/unit/borg/test_rlist.py @@ -0,0 +1,398 @@ +import argparse +import logging + +import pytest +from flexmock import flexmock + +from borgmatic.borg import rlist as module + +from ..test_verbosity import insert_logging_mock + +BORG_LIST_LATEST_ARGUMENTS = ( + '--last', + '1', + '--short', + 'repo', +) + + +def test_resolve_archive_name_passes_through_non_latest_archive_name(): + archive = 'myhost-2030-01-01T14:41:17.647620' + + assert ( + module.resolve_archive_name('repo', archive, storage_config={}, local_borg_version='1.2.3') + == archive + ) + + +def test_resolve_archive_name_calls_borg_with_parameters(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + insert_logging_mock(logging.INFO) + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + insert_logging_mock(logging.DEBUG) + + assert ( + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + == expected_archive + ) + + +def test_resolve_archive_name_with_local_path_calls_borg_via_local_path(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg1', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={}, local_borg_version='1.2.3', local_path='borg1' + ) + == expected_archive + ) + + +def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters(): + expected_archive = 'archive-name' + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={}, local_borg_version='1.2.3', remote_path='borg1' + ) + == expected_archive + ) + + +def test_resolve_archive_name_without_archives_raises(): + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return('') + + with pytest.raises(ValueError): + module.resolve_archive_name('repo', 'latest', storage_config={}, local_borg_version='1.2.3') + + +def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters(): + expected_archive = 'archive-name' + + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS, + output_log_level=None, + borg_local_path='borg', + extra_environment=None, + ).and_return(expected_archive + '\n') + + assert ( + module.resolve_archive_name( + 'repo', 'latest', storage_config={'lock_wait': 'okay'}, local_borg_version='1.2.3' + ) + == expected_archive + ) + + +def test_make_rlist_command_includes_log_info(): + insert_logging_mock(logging.INFO) + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), + ) + + assert command == ('borg', 'list', '--info', 'repo') + + +def test_make_rlist_command_includes_json_but_not_info(): + insert_logging_mock(logging.INFO) + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_log_debug(): + insert_logging_mock(logging.DEBUG) + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), + ) + + assert command == ('borg', 'list', '--debug', '--show-rc', 'repo') + + +def test_make_rlist_command_includes_json_but_not_debug(): + insert_logging_mock(logging.DEBUG) + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_json(): + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=True, prefix=None), + ) + + assert command == ('borg', 'list', '--json', 'repo') + + +def test_make_rlist_command_includes_lock_wait(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return( + ('--lock-wait', '5') + ).and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={'lock_wait': 5}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), + ) + + assert command == ('borg', 'list', '--lock-wait', '5', 'repo') + + +def test_make_rlist_command_includes_local_path(): + 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',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), + local_path='borg2', + ) + + assert command == ('borg2', 'list', 'repo') + + +def test_make_rlist_command_includes_remote_path(): + flexmock(module.flags).should_receive('make_flags').and_return( + ('--remote-path', 'borg2') + ).and_return(()).and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None), + remote_path='borg2', + ) + + assert command == ('borg', 'list', '--remote-path', 'borg2', 'repo') + + +def test_make_rlist_command_transforms_prefix_into_glob_archives(): + flexmock(module.flags).should_receive('make_flags').and_return(()).and_return(()).and_return( + ('--glob-archives', 'foo*') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix='foo'), + ) + + assert command == ('borg', 'list', '--glob-archives', 'foo*', 'repo') + + +def test_make_rlist_command_includes_short(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--short',)) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock(archive=None, paths=None, json=False, prefix=None, short=True), + ) + + assert command == ('borg', 'list', '--short', 'repo') + + +@pytest.mark.parametrize( + 'argument_name', + ( + 'glob_archives', + 'sort_by', + 'first', + 'last', + 'exclude', + 'exclude_from', + 'pattern', + 'patterns_from', + ), +) +def test_make_rlist_command_includes_additional_flags(argument_name): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (f"--{argument_name.replace('_', '-')}", 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',)) + + command = module.make_rlist_command( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=flexmock( + archive=None, + paths=None, + json=False, + prefix=None, + find_paths=None, + format=None, + **{argument_name: 'value'}, + ), + ) + + assert command == ('borg', 'list', '--' + argument_name.replace('_', '-'), 'value', 'repo') + + +def test_list_repository_calls_borg_with_parameters(): + rlist_arguments = argparse.Namespace(json=False) + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'rlist', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ).once() + + module.list_repository( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + ) + + +def test_list_repository_with_json_returns_borg_output(): + rlist_arguments = argparse.Namespace(json=True) + json_output = flexmock() + + flexmock(module.feature).should_receive('available').and_return(False) + flexmock(module).should_receive('make_rlist_command').with_args( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + local_path='borg', + remote_path=None, + ).and_return(('borg', 'rlist', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').and_return(json_output) + + assert ( + module.list_repository( + repository='repo', + storage_config={}, + local_borg_version='1.2.3', + rlist_arguments=rlist_arguments, + ) + == json_output + ) diff --git a/tests/unit/borg/test_transfer.py b/tests/unit/borg/test_transfer.py new file mode 100644 index 000000000..7e605639b --- /dev/null +++ b/tests/unit/borg/test_transfer.py @@ -0,0 +1,267 @@ +import logging + +import pytest +from flexmock import flexmock + +from borgmatic.borg import transfer as module + +from ..test_verbosity import insert_logging_mock + + +def test_transfer_archives_calls_borg_with_flags(): + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_dry_run_calls_borg_with_dry_run_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('dry-run', True).and_return( + ('--dry-run',) + ) + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo', '--dry-run'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=True, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_log_info_calls_borg_with_info_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', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--info', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.INFO) + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_log_debug_calls_borg_with_debug_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', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--debug', '--show-rc', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + insert_logging_mock(logging.DEBUG) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_archive_calls_borg_with_glob_archives_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'archive' + ).and_return(('--glob-archives', 'archive')) + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--glob-archives', 'archive', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive='archive', glob_archives=None, source_repository=None), + ) + + +def test_transfer_archives_with_glob_archives_calls_borg_with_glob_archives_flag(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'glob-archives', 'foo*' + ).and_return(('--glob-archives', 'foo*')) + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--glob-archives', 'foo*', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives='foo*', source_repository=None), + ) + + +def test_transfer_archives_with_local_path_calls_borg_via_local_path(): + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg2', 'transfer', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg2', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + local_path='borg2', + ) + + +def test_transfer_archives_with_remote_path_calls_borg_with_remote_path_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args( + 'remote-path', 'borg2' + ).and_return(('--remote-path', 'borg2')) + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--remote-path', 'borg2', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + remote_path='borg2', + ) + + +def test_transfer_archives_with_lock_wait_calls_borg_with_lock_wait_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return( + ('--lock-wait', '5') + ) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(()) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + storage_config = {'lock_wait': 5} + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--lock-wait', '5', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config=storage_config, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository=None), + ) + + +@pytest.mark.parametrize('argument_name', ('upgrader', 'sort_by', 'first', 'last')) +def test_transfer_archives_passes_through_arguments_to_borg(argument_name): + flag_name = f"--{argument_name.replace('_', ' ')}" + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags_from_arguments').and_return( + (flag_name, 'value') + ) + flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo')) + flexmock(module.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', flag_name, 'value', '--repo', 'repo'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock( + archive=None, glob_archives=None, source_repository=None, **{argument_name: 'value'} + ), + ) + + +def test_transfer_archives_with_source_repository_calls_borg_with_other_repo_flags(): + flexmock(module.flags).should_receive('make_flags').and_return(()) + flexmock(module.flags).should_receive('make_flags').with_args('other-repo', 'other').and_return( + ('--other-repo', 'other') + ) + 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.environment).should_receive('make_environment') + flexmock(module).should_receive('execute_command').with_args( + ('borg', 'transfer', '--repo', 'repo', '--other-repo', 'other'), + output_log_level=logging.WARNING, + borg_local_path='borg', + extra_environment=None, + ) + + module.transfer_archives( + dry_run=False, + repository='repo', + storage_config={}, + local_borg_version='2.3.4', + transfer_arguments=flexmock(archive=None, glob_archives=None, source_repository='other'), + ) diff --git a/tests/unit/commands/test_borgmatic.py b/tests/unit/commands/test_borgmatic.py index bc62d7a78..5a49aff8a 100644 --- a/tests/unit/commands/test_borgmatic.py +++ b/tests/unit/commands/test_borgmatic.py @@ -340,12 +340,17 @@ def test_run_configuration_retries_timeout_multiple_repos(): assert results == error_logs -def test_run_actions_does_not_raise_for_init_action(): - flexmock(module.borg_init).should_receive('initialize_repository') +def test_run_actions_does_not_raise_for_rcreate_action(): + flexmock(module.borg_rcreate).should_receive('create_repository') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), - 'init': flexmock( - encryption_mode=flexmock(), append_only=flexmock(), storage_quota=flexmock() + 'rcreate': flexmock( + encryption_mode=flexmock(), + source_repository=flexmock(), + copy_crypt_key=flexmock(), + append_only=flexmock(), + storage_quota=flexmock(), + make_parent_dirs=flexmock(), ), } @@ -366,6 +371,30 @@ def test_run_actions_does_not_raise_for_init_action(): ) +def test_run_actions_does_not_raise_for_transfer_action(): + flexmock(module.borg_transfer).should_receive('transfer_archives') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'transfer': flexmock(), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_calls_hooks_for_prune_action(): flexmock(module.borg_prune).should_receive('prune_archives') flexmock(module.command).should_receive('execute_hook').twice() @@ -571,10 +600,35 @@ def test_run_actions_does_not_raise_for_mount_action(): ) +def test_run_actions_does_not_raise_for_rlist_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_rlist).should_receive('list_repository') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'rlist': flexmock(repository=flexmock(), json=flexmock()), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_does_not_raise_for_list_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) - flexmock(module.borg_list).should_receive('list_archives') + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_list).should_receive('list_archive') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), 'list': flexmock(repository=flexmock(), archive=flexmock(), json=flexmock()), @@ -597,9 +651,34 @@ def test_run_actions_does_not_raise_for_list_action(): ) +def test_run_actions_does_not_raise_for_rinfo_action(): + flexmock(module.validate).should_receive('repositories_match').and_return(True) + flexmock(module.borg_rinfo).should_receive('display_repository_info') + arguments = { + 'global': flexmock(monitoring_verbosity=1, dry_run=False), + 'rinfo': flexmock(repository=flexmock(), json=flexmock()), + } + + list( + module.run_actions( + arguments=arguments, + config_filename='test.yaml', + location={'repositories': ['repo']}, + storage={}, + retention={}, + consistency={}, + hooks={}, + local_path=None, + remote_path=None, + local_borg_version=None, + repository_path='repo', + ) + ) + + def test_run_actions_does_not_raise_for_info_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) flexmock(module.borg_info).should_receive('display_archives_info') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), @@ -625,7 +704,7 @@ def test_run_actions_does_not_raise_for_info_action(): def test_run_actions_does_not_raise_for_borg_action(): flexmock(module.validate).should_receive('repositories_match').and_return(True) - flexmock(module.borg_list).should_receive('resolve_archive_name').and_return(flexmock()) + flexmock(module.borg_rlist).should_receive('resolve_archive_name').and_return(flexmock()) flexmock(module.borg_borg).should_receive('run_arbitrary_borg') arguments = { 'global': flexmock(monitoring_verbosity=1, dry_run=False), @@ -649,17 +728,19 @@ def test_run_actions_does_not_raise_for_borg_action(): ) -def test_load_configurations_collects_parsed_configurations(): +def test_load_configurations_collects_parsed_configurations_and_logs(): configuration = flexmock() other_configuration = flexmock() + test_expected_logs = [flexmock(), flexmock()] + other_expected_logs = [flexmock(), flexmock()] flexmock(module.validate).should_receive('parse_configuration').and_return( - configuration - ).and_return(other_configuration) + configuration, test_expected_logs + ).and_return(other_configuration, other_expected_logs) configs, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml'))) assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration} - assert logs == [] + assert logs == test_expected_logs + other_expected_logs def test_load_configurations_logs_warning_for_permission_error(): @@ -746,6 +827,7 @@ def test_get_local_path_without_local_path_defaults_to_borg(): def test_collect_configuration_run_summary_logs_info_for_success(): flexmock(module.command).should_receive('execute_hook').never() + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {} @@ -757,6 +839,7 @@ def test_collect_configuration_run_summary_logs_info_for_success(): def test_collect_configuration_run_summary_executes_hooks_for_create(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)} @@ -768,6 +851,7 @@ def test_collect_configuration_run_summary_executes_hooks_for_create(): def test_collect_configuration_run_summary_logs_info_for_success_with_extract(): + flexmock(module.validate).should_receive('guard_single_repository_selected') flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'extract': flexmock(repository='repo')} @@ -795,6 +879,7 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error(): def test_collect_configuration_run_summary_logs_info_for_success_with_mount(): + flexmock(module.validate).should_receive('guard_single_repository_selected') flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'mount': flexmock(repository='repo')} @@ -846,6 +931,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error(): def test_collect_configuration_run_summary_logs_post_hook_error(): flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError) + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) expected_logs = (flexmock(),) flexmock(module).should_receive('log_error_records').and_return(expected_logs) @@ -874,6 +960,7 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi def test_collect_configuration_run_summary_logs_info_for_success_with_list(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return([]) arguments = {'list': flexmock(repository='repo', archive=None)} @@ -916,6 +1003,7 @@ def test_collect_configuration_run_summary_logs_run_umount_error(): def test_collect_configuration_run_summary_logs_outputs_merged_json_results(): + flexmock(module.validate).should_receive('guard_configuration_contains_repository') flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return( ['baz'] ) diff --git a/tests/unit/config/test_normalize.py b/tests/unit/config/test_normalize.py index 0927f9d2e..608824436 100644 --- a/tests/unit/config/test_normalize.py +++ b/tests/unit/config/test_normalize.py @@ -4,44 +4,90 @@ from borgmatic.config import normalize as module @pytest.mark.parametrize( - 'config,expected_config', + 'config,expected_config,produces_logs', ( ( {'location': {'exclude_if_present': '.nobackup'}}, {'location': {'exclude_if_present': ['.nobackup']}}, + False, ), ( {'location': {'exclude_if_present': ['.nobackup']}}, {'location': {'exclude_if_present': ['.nobackup']}}, + False, ), ( {'location': {'source_directories': ['foo', 'bar']}}, {'location': {'source_directories': ['foo', 'bar']}}, + False, + ), + ( + {'storage': {'compression': 'yes_please'}}, + {'storage': {'compression': 'yes_please'}}, + False, ), - ({'storage': {'compression': 'yes_please'}}, {'storage': {'compression': 'yes_please'}}), ( {'hooks': {'healthchecks': 'https://example.com'}}, {'hooks': {'healthchecks': {'ping_url': 'https://example.com'}}}, + False, ), ( {'hooks': {'cronitor': 'https://example.com'}}, {'hooks': {'cronitor': {'ping_url': 'https://example.com'}}}, + False, ), ( {'hooks': {'pagerduty': 'https://example.com'}}, {'hooks': {'pagerduty': {'integration_key': 'https://example.com'}}}, + False, ), ( {'hooks': {'cronhub': 'https://example.com'}}, {'hooks': {'cronhub': {'ping_url': 'https://example.com'}}}, + False, ), ( {'consistency': {'checks': ['archives']}}, {'consistency': {'checks': [{'name': 'archives'}]}}, + False, + ), + ({'location': {'numeric_owner': False}}, {'location': {'numeric_ids': False}}, False,), + ({'location': {'bsd_flags': False}}, {'location': {'flags': False}}, False,), + ( + {'storage': {'remote_rate_limit': False}}, + {'storage': {'upload_rate_limit': False}}, + False, + ), + ( + {'location': {'repositories': ['foo@bar:/repo']}}, + {'location': {'repositories': ['ssh://foo@bar/repo']}}, + True, + ), + ( + {'location': {'repositories': ['foo@bar:repo']}}, + {'location': {'repositories': ['ssh://foo@bar/./repo']}}, + True, + ), + ( + {'location': {'repositories': ['foo@bar:~/repo']}}, + {'location': {'repositories': ['ssh://foo@bar/~/repo']}}, + True, + ), + ( + {'location': {'repositories': ['ssh://foo@bar:1234/repo']}}, + {'location': {'repositories': ['ssh://foo@bar:1234/repo']}}, + False, ), ), ) -def test_normalize_applies_hard_coded_normalization_to_config(config, expected_config): - module.normalize(config) +def test_normalize_applies_hard_coded_normalization_to_config( + config, expected_config, produces_logs +): + logs = module.normalize('test.yaml', config) assert config == expected_config + + if produces_logs: + assert logs + else: + assert logs == [] diff --git a/tests/unit/config/test_validate.py b/tests/unit/config/test_validate.py index a8588992a..713ecc7a2 100644 --- a/tests/unit/config/test_validate.py +++ b/tests/unit/config/test_validate.py @@ -120,14 +120,6 @@ def test_guard_configuration_contains_repository_does_not_raise_when_repository_ ) -def test_guard_configuration_contains_repository_errors_when_repository_assumed_to_match_config_twice(): - with pytest.raises(ValueError): - module.guard_configuration_contains_repository( - repository=None, - configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, - ) - - def test_guard_configuration_contains_repository_errors_when_repository_missing_from_config(): flexmock(module).should_receive('repositories_match').replace_with( lambda first, second: first == second @@ -153,3 +145,30 @@ def test_guard_configuration_contains_repository_errors_when_repository_matches_ 'other.yaml': {'location': {'repositories': ['repo']}}, }, ) + + +def test_guard_single_repository_selected_raises_when_multiple_repositories_configured_and_none_selected(): + with pytest.raises(ValueError): + module.guard_single_repository_selected( + repository=None, + configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_single_repository_configured_and_none_selected(): + module.guard_single_repository_selected( + repository=None, configurations={'config.yaml': {'location': {'repositories': ['repo']}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_no_repositories_configured_and_one_selected(): + module.guard_single_repository_selected( + repository='repo', configurations={'config.yaml': {'location': {'repositories': []}}}, + ) + + +def test_guard_single_repository_selected_does_not_raise_when_repositories_configured_and_one_selected(): + module.guard_single_repository_selected( + repository='repo', + configurations={'config.yaml': {'location': {'repositories': ['repo', 'repo2']}}}, + ) diff --git a/tests/unit/hooks/test_healthchecks.py b/tests/unit/hooks/test_healthchecks.py index 65c5613d3..ee78e52b3 100644 --- a/tests/unit/hooks/test_healthchecks.py +++ b/tests/unit/hooks/test_healthchecks.py @@ -138,7 +138,7 @@ def test_ping_monitor_hits_ping_url_for_start_state(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com'} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -155,7 +155,7 @@ def test_ping_monitor_hits_ping_url_for_finish_state(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com', data=payload.encode('utf-8') + 'https://example.com', data=payload.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -172,7 +172,7 @@ def test_ping_monitor_hits_ping_url_for_fail_state(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/fail', data=payload.encode('utf') + 'https://example.com/fail', data=payload.encode('utf'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -189,7 +189,43 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url(): payload = 'data' flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) flexmock(module.requests).should_receive('post').with_args( - 'https://hc-ping.com/{}'.format(hook_config['ping_url']), data=payload.encode('utf-8') + 'https://hc-ping.com/{}'.format(hook_config['ping_url']), + data=payload.encode('utf-8'), + verify=True, + ).and_return(flexmock(ok=True)) + + module.ping_monitor( + hook_config, + 'config.yaml', + state=module.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_skips_ssl_verification_when_verify_tls_false(): + hook_config = {'ping_url': 'https://example.com', 'verify_tls': False} + payload = 'data' + flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) + flexmock(module.requests).should_receive('post').with_args( + 'https://example.com', data=payload.encode('utf-8'), verify=False + ).and_return(flexmock(ok=True)) + + module.ping_monitor( + hook_config, + 'config.yaml', + state=module.monitor.State.FINISH, + monitoring_log_level=1, + dry_run=False, + ) + + +def test_ping_monitor_executes_ssl_verification_when_verify_tls_true(): + hook_config = {'ping_url': 'https://example.com', 'verify_tls': True} + payload = 'data' + flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload) + flexmock(module.requests).should_receive('post').with_args( + 'https://example.com', data=payload.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -233,7 +269,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(flexmock(ok=True)) module.ping_monitor( @@ -249,7 +285,7 @@ def test_ping_monitor_with_connection_error_logs_warning(): flexmock(module).should_receive('Forgetful_buffering_handler') hook_config = {'ping_url': 'https://example.com'} flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_raise(module.requests.exceptions.ConnectionError) flexmock(module.logger).should_receive('warning').once() @@ -270,7 +306,7 @@ def test_ping_monitor_with_other_error_logs_warning(): module.requests.exceptions.RequestException ) flexmock(module.requests).should_receive('post').with_args( - 'https://example.com/start', data=''.encode('utf-8') + 'https://example.com/start', data=''.encode('utf-8'), verify=True ).and_return(response) flexmock(module.logger).should_receive('warning').once() diff --git a/tests/unit/test_execute.py b/tests/unit/test_execute.py index c747a9497..ab4f15ad5 100644 --- a/tests/unit/test_execute.py +++ b/tests/unit/test_execute.py @@ -289,6 +289,27 @@ def test_execute_command_with_processes_calls_full_command(): assert output is None +def test_execute_command_with_processes_returns_output_with_output_log_level_none(): + full_command = ['foo', 'bar'] + processes = (flexmock(),) + flexmock(module.os, environ={'a': 'b'}) + process = flexmock(stdout=None) + flexmock(module.subprocess).should_receive('Popen').with_args( + full_command, + stdin=None, + stdout=module.subprocess.PIPE, + stderr=module.subprocess.STDOUT, + shell=False, + env=None, + cwd=None, + ).and_return(process).once() + flexmock(module).should_receive('log_outputs').and_return({process: 'out'}) + + output = module.execute_command_with_processes(full_command, processes, output_log_level=None) + + assert output == 'out' + + def test_execute_command_with_processes_calls_full_command_with_output_file(): full_command = ['foo', 'bar'] processes = (flexmock(),)