Compare commits

...
6 Commits
21 changed files with 351 additions and 64 deletions
+14
View File
@@ -1,3 +1,17 @@
1.4.18
* Fix "--repository" flag to accept relative paths.
* Fix "borgmatic umount" so it only runs Borg once instead of once per repository / configuration
file.
* #253: Mount whole repositories via "borgmatic mount" without any "--archive" flag.
* #269: Filter listed paths via "borgmatic list --path" flag.
1.4.17
* #235: Pass extra options directly to particular Borg commands, handy for Borg options that
borgmatic does not yet support natively. Use "extra_borg_options" in the storage configuration
section.
* #266: Attempt to repair any inconsistencies found during a consistency check via
"borgmatic check --repair" flag.
1.4.16
* #256: Fix for "before_backup" hook not triggering an error when the command contains "borg" and
has an exit code of 1.
+15 -7
View File
@@ -1,7 +1,7 @@
import logging
from borgmatic.borg import extract
from borgmatic.execute import execute_command
from borgmatic.execute import execute_command, execute_command_without_capture
DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-'
@@ -91,23 +91,23 @@ def check_archives(
consistency_config,
local_path='borg',
remote_path=None,
repair=None,
only_checks=None,
):
'''
Given a local or remote repository path, a storage config dict, a consistency config dict,
local/remote commands to run, and an optional list of checks to use instead of configured
checks, check the contained Borg archives for consistency.
local/remote commands to run, whether to attempt a repair, and an optional list of checks
to use instead of configured checks, check the contained Borg archives for consistency.
If there are no consistency checks to run, skip running them.
'''
checks = _parse_checks(consistency_config, only_checks)
check_last = consistency_config.get('check_last', None)
lock_wait = None
extra_borg_options = storage_config.get('extra_borg_options', {}).get('check', '')
if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))):
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
lock_wait = storage_config.get('lock_wait', None)
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
verbosity_flags = ()
if logger.isEnabledFor(logging.INFO):
@@ -119,13 +119,21 @@ def check_archives(
full_command = (
(local_path, 'check')
+ (('--repair',) if repair else ())
+ _make_check_flags(checks, check_last, prefix)
+ remote_path_flags
+ lock_wait_flags
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ verbosity_flags
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
)
# The Borg repair option trigger an interactive prompt, which won't work when output is
# captured.
if repair:
execute_command_without_capture(full_command, error_on_warnings=True)
return
execute_command(full_command, error_on_warnings=True)
if 'extract' in checks:
+2
View File
@@ -150,6 +150,7 @@ def create_archive(
files_cache = location_config.get('files_cache')
default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
full_command = (
(local_path, 'create')
@@ -185,6 +186,7 @@ def create_archive(
+ (('--dry-run',) if dry_run else ())
+ (('--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
+7 -3
View File
@@ -11,6 +11,7 @@ INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def initialize_repository(
repository,
storage_config,
encryption_mode,
append_only=None,
storage_quota=None,
@@ -18,9 +19,9 @@ def initialize_repository(
remote_path=None,
):
'''
Given a local or remote repository path, 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.
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.
'''
info_command = (local_path, 'info', repository)
logger.debug(' '.join(info_command))
@@ -33,6 +34,8 @@ def initialize_repository(
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 ())
@@ -41,6 +44,7 @@ def initialize_repository(
+ (('--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,)
)
+2 -1
View File
@@ -36,13 +36,14 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(
list_arguments, excludes=('repository', 'archive', 'successful')
list_arguments, excludes=('repository', 'archive', 'paths', 'successful')
)
+ (
'::'.join((repository, list_arguments.archive))
if list_arguments.archive
else repository,
)
+ (tuple(list_arguments.paths) if list_arguments.paths else ())
)
return execute_command(
+4 -4
View File
@@ -17,9 +17,9 @@ def mount_archive(
remote_path=None,
):
'''
Given a local or remote repository path, an 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.
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.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
@@ -33,7 +33,7 @@ def mount_archive(
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--foreground',) if foreground else ())
+ (('-o', options) if options else ())
+ ('::'.join((repository, archive)),)
+ (('::'.join((repository, archive)),) if archive else (repository,))
+ (mount_point,)
+ (tuple(paths) if paths else ())
)
+2
View File
@@ -49,6 +49,7 @@ def prune_archives(
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('prune', '')
full_command = (
(local_path, 'prune')
@@ -61,6 +62,7 @@ def prune_archives(
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--stats',) if stats else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
)
+15 -1
View File
@@ -266,6 +266,13 @@ def parse_arguments(*unparsed_arguments):
add_help=False,
)
check_group = check_parser.add_argument_group('check arguments')
check_group.add_argument(
'--repair',
dest='repair',
default=False,
action='store_true',
help='Attempt to repair any inconsistencies found (experimental and only for interactive use)',
)
check_group.add_argument(
'--only',
metavar='CHECK',
@@ -326,7 +333,7 @@ def parse_arguments(*unparsed_arguments):
'--repository',
help='Path of repository to use, defaults to the configured repository if there is only one',
)
mount_group.add_argument('--archive', help='Name of archive to mount', required=True)
mount_group.add_argument('--archive', help='Name of archive to mount')
mount_group.add_argument(
'--mount-point',
metavar='PATH',
@@ -412,6 +419,13 @@ def parse_arguments(*unparsed_arguments):
help='Path of repository to list, defaults to the configured repository if there is only one',
)
list_group.add_argument('--archive', help='Name of archive to list')
list_group.add_argument(
'--path',
metavar='PATH',
nargs='+',
dest='paths',
help='Paths to list from archive, defaults to the entire archive',
)
list_group.add_argument(
'--short', default=False, action='store_true', help='Output only archive or path names'
)
+41 -13
View File
@@ -189,6 +189,7 @@ def run_actions(
logger.info('{}: Initializing repository'.format(repository))
borg_init.initialize_repository(
repository,
storage,
arguments['init'].encryption_mode,
arguments['init'].append_only,
arguments['init'].storage_quota,
@@ -229,10 +230,13 @@ def run_actions(
consistency,
local_path=local_path,
remote_path=remote_path,
repair=arguments['check'].repair,
only_checks=arguments['check'].only,
)
if 'extract' in arguments:
if arguments['extract'].repository is None or repository == arguments['extract'].repository:
if arguments['extract'].repository is None or validate.repositories_match(
repository, arguments['extract'].repository
):
logger.info(
'{}: Extracting archive {}'.format(repository, arguments['extract'].archive)
)
@@ -249,8 +253,16 @@ def run_actions(
progress=arguments['extract'].progress,
)
if 'mount' in arguments:
if arguments['mount'].repository is None or repository == arguments['mount'].repository:
logger.info('{}: Mounting archive {}'.format(repository, arguments['mount'].archive))
if arguments['mount'].repository is None or validate.repositories_match(
repository, arguments['mount'].repository
):
if arguments['mount'].archive:
logger.info(
'{}: Mounting archive {}'.format(repository, arguments['mount'].archive)
)
else:
logger.info('{}: Mounting repository'.format(repository))
borg_mount.mount_archive(
repository,
arguments['mount'].archive,
@@ -262,15 +274,10 @@ def run_actions(
local_path=local_path,
remote_path=remote_path,
)
if 'umount' in arguments:
logger.info(
'{}: Unmounting mount point {}'.format(repository, arguments['umount'].mount_point)
)
borg_umount.unmount_archive(
mount_point=arguments['umount'].mount_point, local_path=local_path
)
if 'restore' in arguments:
if arguments['restore'].repository is None or repository == arguments['restore'].repository:
if arguments['restore'].repository is None or validate.repositories_match(
repository, arguments['restore'].repository
):
logger.info(
'{}: Restoring databases from archive {}'.format(
repository, arguments['restore'].archive
@@ -328,7 +335,9 @@ def run_actions(
global_arguments.dry_run,
)
if 'list' in arguments:
if arguments['list'].repository is None or repository == arguments['list'].repository:
if arguments['list'].repository is None or validate.repositories_match(
repository, arguments['list'].repository
):
logger.info('{}: Listing archives'.format(repository))
json_output = borg_list.list_archives(
repository,
@@ -340,7 +349,9 @@ def run_actions(
if json_output:
yield json.loads(json_output)
if 'info' in arguments:
if arguments['info'].repository is None or repository == arguments['info'].repository:
if arguments['info'].repository is None or validate.repositories_match(
repository, arguments['info'].repository
):
logger.info('{}: Displaying summary info for archives'.format(repository))
json_output = borg_info.display_archives_info(
repository,
@@ -429,6 +440,14 @@ def make_error_log_records(message, error=None):
pass
def get_local_path(configs):
'''
Arbitrarily return the local path from the first configuration dict. Default to "borg" if not
set.
'''
return next(iter(configs.values())).get('location', {}).get('local_path', 'borg')
def collect_configuration_run_summary_logs(configs, arguments):
'''
Given a dict of configuration filename to corresponding parsed configuration, and parsed
@@ -499,6 +518,15 @@ def collect_configuration_run_summary_logs(configs, arguments):
if results:
json_results.extend(results)
if 'umount' in arguments:
logger.info('Unmounting mount point {}'.format(arguments['umount'].mount_point))
try:
borg_umount.unmount_archive(
mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs)
)
except (CalledProcessError, OSError) as error:
yield from make_error_log_records('Error unmounting mount point', error)
if json_results:
sys.stdout.write(json.dumps(json_results))
+23
View File
@@ -245,6 +245,29 @@ map:
Bypass Borg error about a previously unknown unencrypted repository. Defaults to
false.
example: true
extra_borg_options:
map:
init:
type: str
desc: Extra command-line options to pass to "borg init".
example: "--make-parent-dirs"
prune:
type: str
desc: Extra command-line options to pass to "borg prune".
example: "--save-space"
create:
type: str
desc: Extra command-line options to pass to "borg create".
example: "--no-files-cache"
check:
type: str
desc: Extra command-line options to pass to "borg check".
example: "--save-space"
desc: |
Additional options to pass directly to particular Borg commands, handy for Borg
options that borgmatic does not yet support natively. Note that borgmatic does
not perform any validation on these options. Running borgmatic with
"--verbosity 2" shows the exact Borg command-line invocation.
retention:
desc: |
Retention policy for how many backups to keep in each category. See
+21 -4
View File
@@ -1,4 +1,5 @@
import logging
import os
import pkg_resources
import pykwalify.core
@@ -112,6 +113,24 @@ def parse_configuration(config_filename, schema_filename):
return parsed_result
def normalize_repository_path(repository):
'''
Given a repository path, return the absolute path of it (for local repositories).
'''
# A colon in the repository indicates it's a remote repository. Bail.
if ':' in repository:
return repository
return os.path.abspath(repository)
def repositories_match(first, second):
'''
Given two repository paths (relative and/or absolute), return whether they match.
'''
return normalize_repository_path(first) == normalize_repository_path(second)
def guard_configuration_contains_repository(repository, configurations):
'''
Given a repository path and a dict mapping from config filename to corresponding parsed config
@@ -133,9 +152,7 @@ def guard_configuration_contains_repository(repository, configurations):
if count > 1:
raise ValueError(
'Can\'t determine which repository to use. Use --repository option to disambiguate'.format(
repository
)
'Can\'t determine which repository to use. Use --repository option to disambiguate'
)
return
@@ -145,7 +162,7 @@ def guard_configuration_contains_repository(repository, configurations):
config_repository
for config in configurations.values()
for config_repository in config['location']['repositories']
if repository == config_repository
if repositories_match(repository, config_repository)
)
)
+6
View File
@@ -100,6 +100,12 @@ borgmatic mount --archive host-2019-... --mount-point /mnt
This mounts the entire archive on the given mount point `/mnt`, so that you
can look in there for your files.
Omit the `--archive` flag to mount all archives (lazy-loaded):
```bash
borgmatic mount --mount-point /mnt
```
If you'd like to restrict the mounted filesystem to only particular paths from
your archive, use the `--path` flag, similar to the `extract` action above.
For instance:
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.4.16'
VERSION = '1.4.18'
setup(
@@ -352,13 +352,6 @@ def test_parse_arguments_requires_archive_with_extract():
module.parse_arguments('--config', 'myconfig', 'extract')
def test_parse_arguments_requires_archive_with_mount():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(SystemExit):
module.parse_arguments('--config', 'myconfig', 'mount', '--mount-point', '/mnt')
def test_parse_arguments_requires_archive_with_restore():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
+29
View File
@@ -158,6 +158,21 @@ def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag():
assert flags == ('--prefix', 'foo-')
def test_check_archives_with_repair_calls_borg_with_repair_parameter():
checks = ('repository',)
consistency_config = {'check_last': None}
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').and_return(())
flexmock(module).should_receive('execute_command').never()
flexmock(module).should_receive('execute_command_without_capture').with_args(
('borg', 'check', '--repair', 'repo'), error_on_warnings=True
).once()
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config, repair=True
)
@pytest.mark.parametrize(
'checks',
(
@@ -296,3 +311,17 @@ def test_check_archives_with_retention_prefix():
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
)
def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
checks = ('repository',)
consistency_config = {'check_last': None}
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
module.check_archives(
repository='repo',
storage_config={'extra_borg_options': {'check': '--extra --options'}},
consistency_config=consistency_config,
)
+25
View File
@@ -1092,3 +1092,28 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
},
storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},
)
def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', '--extra', '--options') + ARCHIVE_WITH_PATHS,
output_log_level=logging.INFO,
error_on_warnings=False,
)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={'extra_borg_options': {'create': '--extra --options'}},
)
+33 -10
View File
@@ -32,7 +32,7 @@ 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', encryption_mode='repokey')
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_raises_for_borg_init_error():
@@ -42,14 +42,16 @@ def test_initialize_repository_raises_for_borg_init_error():
)
with pytest.raises(subprocess.CalledProcessError):
module.initialize_repository(repository='repo', encryption_mode='repokey')
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()
flexmock(module).should_receive('execute_command_without_capture').never()
module.initialize_repository(repository='repo', encryption_mode='repokey')
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_raises_for_unknown_info_command_error():
@@ -58,21 +60,27 @@ def test_initialize_repository_raises_for_unknown_info_command_error():
)
with pytest.raises(subprocess.CalledProcessError):
module.initialize_repository(repository='repo', encryption_mode='repokey')
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', encryption_mode='repokey', append_only=True)
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', encryption_mode='repokey', storage_quota='5G')
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():
@@ -80,7 +88,7 @@ def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
insert_init_command_mock(INIT_COMMAND + ('--info', 'repo'))
insert_logging_mock(logging.INFO)
module.initialize_repository(repository='repo', encryption_mode='repokey')
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
@@ -88,18 +96,33 @@ def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo'))
insert_logging_mock(logging.DEBUG)
module.initialize_repository(repository='repo', encryption_mode='repokey')
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', encryption_mode='repokey', local_path='borg1')
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', encryption_mode='repokey', remote_path='borg1')
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',
)
+28 -13
View File
@@ -16,7 +16,7 @@ def test_list_archives_calls_borg_with_parameters():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
)
@@ -31,7 +31,7 @@ def test_list_archives_with_log_info_calls_borg_with_info_parameter():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
)
@@ -44,7 +44,7 @@ def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=True, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=True, successful=False),
)
@@ -59,7 +59,7 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
)
@@ -72,7 +72,7 @@ def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=True, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=True, successful=False),
)
@@ -87,7 +87,7 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
module.list_archives(
repository='repo',
storage_config=storage_config,
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
)
@@ -100,7 +100,22 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
module.list_archives(
repository='repo',
storage_config=storage_config,
list_arguments=flexmock(archive='archive', json=False, successful=False),
list_arguments=flexmock(archive='archive', paths=None, json=False, successful=False),
)
def test_list_archives_with_path_calls_borg_with_path_parameter():
storage_config = {}
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive', 'var/lib'),
output_log_level=logging.WARNING,
error_on_warnings=False,
)
module.list_archives(
repository='repo',
storage_config=storage_config,
list_arguments=flexmock(archive='archive', paths=['var/lib'], json=False, successful=False),
)
@@ -112,7 +127,7 @@ def test_list_archives_with_local_path_calls_borg_via_local_path():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
local_path='borg1',
)
@@ -127,7 +142,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False),
remote_path='borg1',
)
@@ -142,7 +157,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=False, short=True),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=False, short=True),
)
@@ -171,7 +186,7 @@ def test_list_archives_passes_through_arguments_to_borg(argument_name):
repository='repo',
storage_config={},
list_arguments=flexmock(
archive=None, json=False, successful=False, **{argument_name: 'value'}
archive=None, paths=None, json=False, successful=False, **{argument_name: 'value'}
),
)
@@ -186,7 +201,7 @@ def test_list_archives_with_successful_calls_borg_to_exclude_checkpoints():
module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=False, successful=True),
list_arguments=flexmock(archive=None, paths=None, json=False, successful=True),
)
@@ -198,7 +213,7 @@ def test_list_archives_with_json_calls_borg_with_json_parameter():
json_output = module.list_archives(
repository='repo',
storage_config={},
list_arguments=flexmock(archive=None, json=True, successful=False),
list_arguments=flexmock(archive=None, paths=None, json=True, successful=False),
)
assert json_output == '[]'
+15
View File
@@ -188,3 +188,18 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config=storage_config,
retention_config=retention_config,
)
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(
BASE_PRUNE_FLAGS
)
insert_execute_command_mock(PRUNE_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
module.prune_archives(
dry_run=False,
repository='repo',
storage_config={'extra_borg_options': {'prune': '--extra --options'}},
retention_config=retention_config,
)
+28
View File
@@ -169,6 +169,18 @@ def test_make_error_log_records_generates_nothing_for_other_error():
assert logs == ()
def test_get_local_path_uses_configuration_value():
assert module.get_local_path({'test.yaml': {'location': {'local_path': 'borg1'}}}) == 'borg1'
def test_get_local_path_without_location_defaults_to_borg():
assert module.get_local_path({'test.yaml': {}}) == 'borg'
def test_get_local_path_without_local_path_defaults_to_borg():
assert module.get_local_path({'test.yaml': {'location': {}}}) == 'borg'
def test_collect_configuration_run_summary_logs_info_for_success():
flexmock(module.command).should_receive('execute_hook').never()
flexmock(module).should_receive('run_configuration').and_return([])
@@ -324,6 +336,22 @@ def test_collect_configuration_run_summary_logs_run_configuration_error():
assert {log.levelno for log in logs} == {logging.CRITICAL}
def test_collect_configuration_run_summary_logs_run_umount_error():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_return([])
flexmock(module.borg_umount).should_receive('unmount_archive').and_raise(OSError)
flexmock(module).should_receive('make_error_log_records').and_return(
[logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
)
arguments = {'umount': flexmock(mount_point='/mnt')}
logs = tuple(
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
)
assert {log.levelno for log in logs} == {logging.INFO, logging.CRITICAL}
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
['baz']
+40
View File
@@ -1,4 +1,5 @@
import pytest
from flexmock import flexmock
from borgmatic.config import validate as module
@@ -95,7 +96,38 @@ def test_remove_examples_strips_examples_from_sequence_of_maps():
assert schema == {'seq': [{'map': {'foo': {'desc': 'thing'}}}]}
def test_normalize_repository_path_passes_through_remote_repository():
repository = 'example.org:test.borg'
module.normalize_repository_path(repository) == repository
def test_normalize_repository_path_passes_through_absolute_repository():
repository = '/foo/bar/test.borg'
flexmock(module.os.path).should_receive('abspath').and_return(repository)
module.normalize_repository_path(repository) == repository
def test_normalize_repository_path_resolves_relative_repository():
repository = 'test.borg'
absolute = '/foo/bar/test.borg'
flexmock(module.os.path).should_receive('abspath').and_return(absolute)
module.normalize_repository_path(repository) == absolute
def test_repositories_match_does_not_raise():
flexmock(module).should_receive('normalize_repository_path')
module.repositories_match('foo', 'bar')
def test_guard_configuration_contains_repository_does_not_raise_when_repository_in_config():
flexmock(module).should_receive('repositories_match').replace_with(
lambda first, second: first == second
)
module.guard_configuration_contains_repository(
repository='repo', configurations={'config.yaml': {'location': {'repositories': ['repo']}}}
)
@@ -116,6 +148,10 @@ def test_guard_configuration_contains_repository_errors_when_repository_assumed_
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
)
with pytest.raises(ValueError):
module.guard_configuration_contains_repository(
repository='nope',
@@ -124,6 +160,10 @@ def test_guard_configuration_contains_repository_errors_when_repository_missing_
def test_guard_configuration_contains_repository_errors_when_repository_matches_config_twice():
flexmock(module).should_receive('repositories_match').replace_with(
lambda first, second: first == second
)
with pytest.raises(ValueError):
module.guard_configuration_contains_repository(
repository='repo',