Compare commits

...

41 Commits

Author SHA1 Message Date
5b3cfc542d Switch to PyPI API token. 2022-03-14 14:00:03 -07:00
c838c1d11b Fix header placement in documentation guide. 2022-03-14 13:50:22 -07:00
4d1d8d7409 Bump version for release. 2022-03-14 13:43:24 -07:00
db7499db82 Document "repositories" context to for "before_*" and "after_*" command action hooks (#469). 2022-03-14 13:34:14 -07:00
6b500c2a8b Add repositories context for command hooks.
Reviewed-on: borgmatic-collective/borgmatic#469
2022-03-14 20:13:15 +00:00
95c518e59b Documentation tip about dealing with hangs when database hook is enabled. 2022-03-12 13:17:32 -08:00
976516d0e1 When loading a configuration file that is unreadable due to file permissions, warn instead of erroring (#444). 2022-03-08 10:19:36 -08:00
574eb91921 Fix Borg usage error in the "compact" action when running "borgmatic --dry-run". Now, skip "compact" entirely during a dry run (#507). 2022-03-07 21:46:12 -08:00
28fef3264b Fix handling of "patterns_from" and "exclude_from" options to error instead of warning when referencing unreadable files and running "create" action (#486). 2022-03-07 15:32:07 -08:00
9161dbcb7d Removing unnecessary leading underscores from functions. 2022-03-07 11:58:29 -08:00
4b3027e4fc Add test for new working_directory option (#431). 2022-03-03 11:48:18 -08:00
0eb2634f9b Working directory option to support source directories with relative paths (#431).
Reviewed-on: borgmatic-collective/borgmatic#477
2022-03-03 19:28:17 +00:00
7c5b68c98f Bump version for release. 2022-02-10 10:29:18 -08:00
9317cbaaf0 Code formatting. 2022-02-10 10:23:34 -08:00
1b5f04b79f When using the "remote_rate_limit" option, tailor the flags passed to Borg depending on the Borg version (#394). 2022-02-10 10:16:09 -08:00
948c86f62c When using the "numeric_owner" option with the "extract" action, tailor the flags passed to Borg depending on the Borg version (#394). 2022-02-10 10:09:18 -08:00
7e7209322a When using the "numeric_owner" option, tailor the flags passed to Borg depending on the Borg version (#394). 2022-02-10 09:51:13 -08:00
00a57fd947 Code formatting. 2022-02-09 21:20:28 -08:00
6bf6ac310b When using the "bsd_flags" option, tailor the flags passed to Borg depending on the Borg version (#394). 2022-02-09 21:11:00 -08:00
4b5af2770d When the "atime" option is used, tailor the flags passed to Borg depending on version (#394). 2022-02-09 16:54:35 -08:00
b525e70e1c Run "compact" action by default when no actions are specified (#394). 2022-02-09 14:33:12 -08:00
4498671233 Remove references to removed long-deprecated options (#394). 2022-02-09 11:08:02 -08:00
9997aa9a92 Fix capitalization on compact help. 2022-02-08 15:58:09 -08:00
cbf7284f64 Add compact action to command-line reference documentation. 2022-02-08 15:37:24 -08:00
ee466f870d Fixing ruamel.yaml.clib breakages harder. 2022-02-08 13:21:11 -08:00
e3f4bf0293 Build fix for ruamel.yaml.clib error. 2022-02-08 12:52:45 -08:00
46688f10b1 Merge branch 'master' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2022-02-08 12:10:57 -08:00
48f44d2f3d Add tests for compact action (#394). 2022-02-08 12:05:02 -08:00
bff1347ba3 Fix some test failures (#394). 2022-02-08 09:35:03 -08:00
9582324c88 Compact repository segments with new "borgmatic compact" action (#394). 2022-02-07 23:29:44 -08:00
bb0716421d Add comment about systemd service setting that may interfere with external commands in hooks (#492). 2022-01-25 09:26:11 -08:00
bec73245e9 Fix traceback when a YAML validation error occurs (#480, #482). 2022-01-19 20:39:03 -08:00
dcead12e86 Attempt to fix documentation build error introduced by Eleventy upgrade. 2022-01-09 14:21:27 -08:00
0119514c11 Add Python version requirements to setup.py. 2022-01-09 10:19:53 -08:00
b39f08694d Merge branch 'master' into pr-working-directory 2022-01-05 09:30:27 +00:00
Fabian Schilling
85e0334826 Add missing working_directory arg to pass tests 2021-12-10 18:24:41 +01:00
Fabian Schilling
2a80e48a92 Pass working directory to execute functions 2021-12-10 18:23:44 +01:00
Fabian Schilling
5821c6782e Add defaults to not set in schema 2021-12-10 18:23:08 +01:00
Fabian Schilling
f15498f6d9 Add working_directory to borgmatic schema 2021-12-10 17:58:27 +01:00
0014b149f8 remove configuration_filename as it's already set. 2021-11-26 11:38:58 +08:00
091c07bbe2 Add context for various hooks. 2021-11-26 11:35:10 +08:00
34 changed files with 1321 additions and 381 deletions

View File

@ -36,6 +36,8 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy({"docs/static": "static"}); eleventyConfig.addPassthroughCopy({"docs/static": "static"});
eleventyConfig.setLiquidOptions({dynamicPartials: false});
return { return {
templateFormats: [ templateFormats: [
"md", "md",

21
NEWS
View File

@ -1,3 +1,24 @@
1.5.24
* #431: Add "working_directory" option to support source directories with relative paths.
* #444: When loading a configuration file that is unreadable due to file permissions, warn instead
of erroring. This supports running borgmatic as a non-root user with configuration in ~/.config
even if there is an unreadable global configuration file in /etc.
* #469: Add "repositories" context to "before_*" and "after_*" command action hooks. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #486: Fix handling of "patterns_from" and "exclude_from" options to error instead of warning when
referencing unreadable files and "create" action is run.
* #507: Fix Borg usage error in the "compact" action when running "borgmatic --dry-run". Now, skip
"compact" entirely during a dry run.
1.5.23
* #394: Compact repository segments and free space with new "borgmatic compact" action. Borg 1.2+
only. Also run "compact" by default when no actions are specified, as "prune" in Borg 1.2 no
longer frees up space unless "compact" is run.
* #394: When using the "atime", "bsd_flags", "numeric_owner", or "remote_rate_limit" options,
tailor the flags passed to Borg depending on the Borg version.
* #480, #482: Fix traceback when a YAML validation error occurs.
1.5.22 1.5.22
* #288: Add database dump hook for MongoDB. * #288: Add database dump hook for MongoDB.
* #470: Move mysqldump options to the beginning of the command due to MySQL bug 30994. * #470: Move mysqldump options to the beginning of the command due to MySQL bug 30994.

41
borgmatic/borg/compact.py Normal file
View File

@ -0,0 +1,41 @@
import logging
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def compact_segments(
dry_run,
repository,
storage_config,
local_path='borg',
remote_path=None,
progress=False,
cleanup_commits=False,
threshold=None,
):
'''
Given dry-run flag, a local or remote repository path, and a storage config dict, compact Borg
segments in a repository.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('compact', '')
full_command = (
(local_path, 'compact')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--progress',) if progress else ())
+ (('--cleanup-commits',) if cleanup_commits else ())
+ (('--threshold', str(threshold)) if threshold else ())
+ (('--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,)
)
if not dry_run:
execute_command(full_command, output_log_level=logging.INFO, borg_local_path=local_path)

View File

@ -5,12 +5,13 @@ import os
import pathlib import pathlib
import tempfile import tempfile
from borgmatic.borg import feature
from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _expand_directory(directory): def expand_directory(directory):
''' '''
Given a directory path, expand any tilde (representing a user's home directory) and any globs Given a directory path, expand any tilde (representing a user's home directory) and any globs
therein. Return a list of one or more resulting paths. therein. Return a list of one or more resulting paths.
@ -20,7 +21,7 @@ def _expand_directory(directory):
return glob.glob(expanded_directory) or [expanded_directory] return glob.glob(expanded_directory) or [expanded_directory]
def _expand_directories(directories): def expand_directories(directories):
''' '''
Given a sequence of directory paths, expand tildes and globs in each one. Return all the Given a sequence of directory paths, expand tildes and globs in each one. Return all the
resulting directories as a single flattened tuple. resulting directories as a single flattened tuple.
@ -29,11 +30,11 @@ def _expand_directories(directories):
return () return ()
return tuple( return tuple(
itertools.chain.from_iterable(_expand_directory(directory) for directory in directories) itertools.chain.from_iterable(expand_directory(directory) for directory in directories)
) )
def _expand_home_directories(directories): def expand_home_directories(directories):
''' '''
Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing. Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing.
Return the results as a tuple. Return the results as a tuple.
@ -97,7 +98,7 @@ def deduplicate_directories(directory_devices):
return tuple(sorted(deduplicated)) return tuple(sorted(deduplicated))
def _write_pattern_file(patterns=None): def write_pattern_file(patterns=None):
''' '''
Given a sequence of patterns, write them to a named temporary file and return it. Return None Given a sequence of patterns, write them to a named temporary file and return it. Return None
if no patterns are provided. if no patterns are provided.
@ -112,7 +113,19 @@ def _write_pattern_file(patterns=None):
return pattern_file return pattern_file
def _make_pattern_flags(location_config, pattern_filename=None): def ensure_files_readable(*filename_lists):
'''
Given a sequence of filename sequences, ensure that each filename is openable. This prevents
unreadable files from being passed to Borg, which in certain situations only warns instead of
erroring.
'''
for file_object in itertools.chain.from_iterable(
filename_list for filename_list in filename_lists if filename_list
):
open(file_object).close()
def make_pattern_flags(location_config, pattern_filename=None):
''' '''
Given a location config dict with a potential patterns_from option, and a filename containing Given a location config dict with a potential patterns_from option, and a filename containing
any additional patterns, return the corresponding Borg flags for those files as a tuple. any additional patterns, return the corresponding Borg flags for those files as a tuple.
@ -128,7 +141,7 @@ def _make_pattern_flags(location_config, pattern_filename=None):
) )
def _make_exclude_flags(location_config, exclude_filename=None): def make_exclude_flags(location_config, exclude_filename=None):
''' '''
Given a location config dict with various exclude options, and a filename containing any exclude Given a location config dict with various exclude options, and a filename containing any exclude
patterns, return the corresponding Borg flags as a tuple. patterns, return the corresponding Borg flags as a tuple.
@ -187,6 +200,7 @@ def create_archive(
repository, repository,
location_config, location_config,
storage_config, storage_config,
local_borg_version,
local_path='borg', local_path='borg',
remote_path=None, remote_path=None,
progress=False, progress=False,
@ -204,16 +218,20 @@ def create_archive(
''' '''
sources = deduplicate_directories( sources = deduplicate_directories(
map_directories_to_devices( map_directories_to_devices(
_expand_directories( expand_directories(
location_config['source_directories'] location_config['source_directories']
+ borgmatic_source_directories(location_config.get('borgmatic_source_directory')) + borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
) )
) )
) )
pattern_file = _write_pattern_file(location_config.get('patterns')) try:
exclude_file = _write_pattern_file( working_directory = os.path.expanduser(location_config.get('working_directory'))
_expand_home_directories(location_config.get('exclude_patterns')) except TypeError:
working_directory = None
pattern_file = write_pattern_file(location_config.get('patterns'))
exclude_file = write_pattern_file(
expand_home_directories(location_config.get('exclude_patterns'))
) )
checkpoint_interval = storage_config.get('checkpoint_interval', None) checkpoint_interval = storage_config.get('checkpoint_interval', None)
chunker_params = storage_config.get('chunker_params', None) chunker_params = storage_config.get('chunker_params', None)
@ -225,26 +243,52 @@ def create_archive(
archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT) archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '') extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
if feature.available(feature.Feature.ATIME, local_borg_version):
atime_flags = ('--atime',) if location_config.get('atime') is True else ()
else:
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 ()
else:
noflags_flags = ('--nobsdflags',) if location_config.get('bsd_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 ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') 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 ()
)
else:
upload_ratelimit_flags = (
('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
)
ensure_files_readable(location_config.get('patterns_from'), location_config.get('exclude_from'))
full_command = ( full_command = (
tuple(local_path.split(' ')) tuple(local_path.split(' '))
+ ('create',) + ('create',)
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None) + make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None) + make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ()) + (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
+ (('--chunker-params', chunker_params) if chunker_params else ()) + (('--chunker-params', chunker_params) if chunker_params else ())
+ (('--compression', compression) if compression else ()) + (('--compression', compression) if compression else ())
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()) + upload_ratelimit_flags
+ ( + (
('--one-file-system',) ('--one-file-system',)
if location_config.get('one_file_system') or stream_processes if location_config.get('one_file_system') or stream_processes
else () else ()
) )
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ()) + numeric_ids_flags
+ (('--noatime',) if location_config.get('atime') is False else ()) + atime_flags
+ (('--noctime',) if location_config.get('ctime') is False else ()) + (('--noctime',) if location_config.get('ctime') is False else ())
+ (('--nobirthtime',) if location_config.get('birthtime') is False else ()) + (('--nobirthtime',) if location_config.get('birthtime') is False else ())
+ (('--read-special',) if (location_config.get('read_special') or stream_processes) else ()) + (('--read-special',) if (location_config.get('read_special') or stream_processes) else ())
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ()) + noflags_flags
+ (('--files-cache', files_cache) if files_cache else ()) + (('--files-cache', files_cache) if files_cache else ())
+ (('--remote-path', remote_path) if remote_path else ()) + (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ()) + (('--umask', str(umask)) if umask else ())
@ -283,6 +327,13 @@ def create_archive(
output_log_level, output_log_level,
output_file, output_file,
borg_local_path=local_path, borg_local_path=local_path,
working_directory=working_directory,
) )
return execute_command(full_command, output_log_level, output_file, borg_local_path=local_path) return execute_command(
full_command,
output_log_level,
output_file,
borg_local_path=local_path,
working_directory=working_directory,
)

View File

@ -2,6 +2,7 @@ import logging
import os import os
import subprocess import subprocess
from borgmatic.borg import feature
from borgmatic.execute import DO_NOT_CAPTURE, execute_command from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,6 +62,7 @@ def extract_archive(
paths, paths,
location_config, location_config,
storage_config, storage_config,
local_borg_version,
local_path='borg', local_path='borg',
remote_path=None, remote_path=None,
destination_path=None, destination_path=None,
@ -70,9 +72,9 @@ def extract_archive(
): ):
''' '''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
restore from the archive, location/storage configuration dicts, optional local and remote Borg restore from the archive, the local Borg version string, location/storage configuration dicts,
paths, and an optional destination path to extract to, extract the archive into the current optional local and remote Borg paths, and an optional destination path to extract to, extract
directory. the archive into the current directory.
If extract to stdout is True, then start the extraction streaming to stdout, and return that If extract to stdout is True, then start the extraction streaming to stdout, and return that
extract process as an instance of subprocess.Popen. extract process as an instance of subprocess.Popen.
@ -83,10 +85,15 @@ def extract_archive(
if progress and extract_to_stdout: if progress and extract_to_stdout:
raise ValueError('progress and extract_to_stdout cannot both be set') 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 ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else ()
full_command = ( full_command = (
(local_path, 'extract') (local_path, 'extract')
+ (('--remote-path', remote_path) if remote_path else ()) + (('--remote-path', remote_path) if remote_path else ())
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ()) + numeric_ids_flags
+ (('--umask', str(umask)) if umask else ()) + (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ()) + (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ()) + (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())

28
borgmatic/borg/feature.py Normal file
View File

@ -0,0 +1,28 @@
from enum import Enum
from pkg_resources import parse_version
class Feature(Enum):
COMPACT = 1
ATIME = 2
NOFLAGS = 3
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.COMPACT: parse_version('1.2.0a2'), # borg compact
Feature.ATIME: parse_version('1.2.0a7'), # borg create --atime
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
}
def available(feature, borg_version):
'''
Given a Borg Feature constant and a Borg version string, return whether that feature is
available in that version of Borg.
'''
return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse_version(borg_version)

25
borgmatic/borg/version.py Normal file
View File

@ -0,0 +1,25 @@
import logging
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def local_borg_version(local_path='borg'):
'''
Given a local Borg binary path, return a version string for it.
Raise OSError or CalledProcessError if there is a problem running Borg.
Raise ValueError if the version cannot be parsed.
'''
full_command = (
(local_path, '--version')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
)
output = execute_command(full_command, output_log_level=None, borg_local_path=local_path)
try:
return output.split(' ')[1].strip()
except IndexError:
raise ValueError('Could not parse Borg version string')

View File

@ -6,6 +6,7 @@ from borgmatic.config import collect
SUBPARSER_ALIASES = { SUBPARSER_ALIASES = {
'init': ['--init', '-I'], 'init': ['--init', '-I'],
'prune': ['--prune', '-p'], 'prune': ['--prune', '-p'],
'compact': [],
'create': ['--create', '-C'], 'create': ['--create', '-C'],
'check': ['--check', '-k'], 'check': ['--check', '-k'],
'extract': ['--extract', '-x'], 'extract': ['--extract', '-x'],
@ -62,9 +63,9 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
arguments[canonical_name] = parsed arguments[canonical_name] = parsed
# If no actions are explicitly requested, assume defaults: prune, create, and check. # If no actions are explicitly requested, assume defaults: prune, compact, create, and check.
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments: if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
for subparser_name in ('prune', 'create', 'check'): for subparser_name in ('prune', 'compact', 'create', 'check'):
subparser = subparsers[subparser_name] subparser = subparsers[subparser_name]
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments) parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
arguments[subparser_name] = parsed arguments[subparser_name] = parsed
@ -199,8 +200,8 @@ def parse_arguments(*unparsed_arguments):
top_level_parser = ArgumentParser( top_level_parser = ArgumentParser(
description=''' description='''
Simple, configuration-driven backup software for servers and workstations. If none of Simple, configuration-driven backup software for servers and workstations. If none of
the action options are given, then borgmatic defaults to: prune, create, and check the action options are given, then borgmatic defaults to: prune, compact, create, and
archives. check.
''', ''',
parents=[global_parser], parents=[global_parser],
) )
@ -208,7 +209,7 @@ def parse_arguments(*unparsed_arguments):
subparsers = top_level_parser.add_subparsers( subparsers = top_level_parser.add_subparsers(
title='actions', title='actions',
metavar='', metavar='',
help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:', 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_parser = subparsers.add_parser(
'init', 'init',
@ -241,8 +242,8 @@ def parse_arguments(*unparsed_arguments):
prune_parser = subparsers.add_parser( prune_parser = subparsers.add_parser(
'prune', 'prune',
aliases=SUBPARSER_ALIASES['prune'], aliases=SUBPARSER_ALIASES['prune'],
help='Prune archives according to the retention policy', help='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
description='Prune archives according to the retention policy', description='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
add_help=False, add_help=False,
) )
prune_group = prune_parser.add_argument_group('prune arguments') prune_group = prune_parser.add_argument_group('prune arguments')
@ -258,6 +259,38 @@ def parse_arguments(*unparsed_arguments):
) )
prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit') prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
compact_parser = subparsers.add_parser(
'compact',
aliases=SUBPARSER_ALIASES['compact'],
help='Compact segments to free space (Borg 1.2+ only)',
description='Compact segments to free space (Borg 1.2+ only)',
add_help=False,
)
compact_group = compact_parser.add_argument_group('compact arguments')
compact_group.add_argument(
'--progress',
dest='progress',
default=False,
action='store_true',
help='Display progress as each segment is compacted',
)
compact_group.add_argument(
'--cleanup-commits',
dest='cleanup_commits',
default=False,
action='store_true',
help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1',
)
compact_group.add_argument(
'--threshold',
type=int,
dest='threshold',
help='Minimum saved space percentage threshold for compacting a segment, defaults to 10',
)
compact_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
create_parser = subparsers.add_parser( create_parser = subparsers.add_parser(
'create', 'create',
aliases=SUBPARSER_ALIASES['create'], aliases=SUBPARSER_ALIASES['create'],

View File

@ -13,16 +13,19 @@ import pkg_resources
from borgmatic.borg import borg as borg_borg from borgmatic.borg import borg as borg_borg
from borgmatic.borg import check as borg_check from borgmatic.borg import check as borg_check
from borgmatic.borg import compact as borg_compact
from borgmatic.borg import create as borg_create from borgmatic.borg import create as borg_create
from borgmatic.borg import environment as borg_environment from borgmatic.borg import environment as borg_environment
from borgmatic.borg import export_tar as borg_export_tar from borgmatic.borg import export_tar as borg_export_tar
from borgmatic.borg import extract as borg_extract 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 info as borg_info
from borgmatic.borg import init as borg_init from borgmatic.borg import init as borg_init
from borgmatic.borg import list as borg_list from borgmatic.borg import list as borg_list
from borgmatic.borg import mount as borg_mount from borgmatic.borg import mount as borg_mount
from borgmatic.borg import prune as borg_prune from borgmatic.borg import prune as borg_prune
from borgmatic.borg import umount as borg_umount from borgmatic.borg import umount as borg_umount
from borgmatic.borg import version as borg_version
from borgmatic.commands.arguments import parse_arguments from borgmatic.commands.arguments import parse_arguments
from borgmatic.config import checks, collect, convert, validate from borgmatic.config import checks, collect, convert, validate
from borgmatic.hooks import command, dispatch, dump, monitor from borgmatic.hooks import command, dispatch, dump, monitor
@ -38,8 +41,8 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
def run_configuration(config_filename, config, arguments): def run_configuration(config_filename, config, arguments):
''' '''
Given a config filename, the corresponding parsed config dict, and command-line arguments as a Given a config filename, the corresponding parsed config dict, and command-line arguments as a
dict from subparser name to a namespace of parsed arguments, execute its defined pruning, dict from subparser name to a namespace of parsed arguments, execute the defined prune, compact,
backups, consistency checks, and/or other actions. create, check, and/or other actions.
Yield a combination of: Yield a combination of:
@ -59,11 +62,23 @@ def run_configuration(config_filename, config, arguments):
borg_environment.initialize(storage) borg_environment.initialize(storage)
encountered_error = None encountered_error = None
error_repository = '' error_repository = ''
prune_create_or_check = {'prune', 'create', 'check'}.intersection(arguments) using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments)
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity) monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
hook_context = {
'repositories': ','.join(location['repositories']),
}
try: try:
if prune_create_or_check: local_borg_version = borg_version.local_borg_version(local_path)
except (OSError, CalledProcessError, ValueError) as error:
yield from make_error_log_records(
'{}: Error getting local Borg version'.format(config_filename), error
)
return
try:
if using_primary_action:
dispatch.call_hooks( dispatch.call_hooks(
'initialize_monitor', 'initialize_monitor',
hooks, hooks,
@ -79,6 +94,15 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'pre-prune', 'pre-prune',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
)
if 'compact' in arguments:
command.execute_hook(
hooks.get('before_compact'),
hooks.get('umask'),
config_filename,
'pre-compact',
global_arguments.dry_run,
) )
if 'create' in arguments: if 'create' in arguments:
command.execute_hook( command.execute_hook(
@ -87,6 +111,7 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'pre-backup', 'pre-backup',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if 'check' in arguments: if 'check' in arguments:
command.execute_hook( command.execute_hook(
@ -95,6 +120,7 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'pre-check', 'pre-check',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if 'extract' in arguments: if 'extract' in arguments:
command.execute_hook( command.execute_hook(
@ -103,8 +129,9 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'pre-extract', 'pre-extract',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if prune_create_or_check: if using_primary_action:
dispatch.call_hooks( dispatch.call_hooks(
'ping_monitor', 'ping_monitor',
hooks, hooks,
@ -144,6 +171,7 @@ def run_configuration(config_filename, config, arguments):
hooks=hooks, hooks=hooks,
local_path=local_path, local_path=local_path,
remote_path=remote_path, remote_path=remote_path,
local_borg_version=local_borg_version,
repository_path=repository_path, repository_path=repository_path,
) )
except (OSError, CalledProcessError, ValueError) as error: except (OSError, CalledProcessError, ValueError) as error:
@ -168,6 +196,15 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'post-prune', 'post-prune',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
)
if 'compact' in arguments:
command.execute_hook(
hooks.get('after_compact'),
hooks.get('umask'),
config_filename,
'post-compact',
global_arguments.dry_run,
) )
if 'create' in arguments: if 'create' in arguments:
dispatch.call_hooks( dispatch.call_hooks(
@ -184,6 +221,7 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'post-backup', 'post-backup',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if 'check' in arguments: if 'check' in arguments:
command.execute_hook( command.execute_hook(
@ -192,6 +230,7 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'post-check', 'post-check',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if 'extract' in arguments: if 'extract' in arguments:
command.execute_hook( command.execute_hook(
@ -200,8 +239,9 @@ def run_configuration(config_filename, config, arguments):
config_filename, config_filename,
'post-extract', 'post-extract',
global_arguments.dry_run, global_arguments.dry_run,
**hook_context,
) )
if prune_create_or_check: if using_primary_action:
dispatch.call_hooks( dispatch.call_hooks(
'ping_monitor', 'ping_monitor',
hooks, hooks,
@ -228,7 +268,7 @@ def run_configuration(config_filename, config, arguments):
'{}: Error running post hook'.format(config_filename), error '{}: Error running post hook'.format(config_filename), error
) )
if encountered_error and prune_create_or_check: if encountered_error and using_primary_action:
try: try:
command.execute_hook( command.execute_hook(
hooks.get('on_error'), hooks.get('on_error'),
@ -276,12 +316,13 @@ def run_actions(
hooks, hooks,
local_path, local_path,
remote_path, remote_path,
local_borg_version,
repository_path, repository_path,
): # pragma: no cover ): # pragma: no cover
''' '''
Given parsed command-line arguments as an argparse.ArgumentParser instance, several different Given parsed command-line arguments as an argparse.ArgumentParser instance, several different
configuration dicts, local and remote paths to Borg, and a repository name, run all actions configuration dicts, local and remote paths to Borg, a local Borg version string, and a
from the command-line arguments on the given repository. repository name, run all actions from the command-line arguments on the given repository.
Yield JSON output strings from executing any actions that produce JSON. Yield JSON output strings from executing any actions that produce JSON.
@ -314,6 +355,23 @@ def run_actions(
stats=arguments['prune'].stats, stats=arguments['prune'].stats,
files=arguments['prune'].files, files=arguments['prune'].files,
) )
if 'compact' in arguments:
if borg_feature.available(borg_feature.Feature.COMPACT, local_borg_version):
logger.info('{}: Compacting segments{}'.format(repository, dry_run_label))
borg_compact.compact_segments(
global_arguments.dry_run,
repository,
storage,
local_path=local_path,
remote_path=remote_path,
progress=arguments['compact'].progress,
cleanup_commits=arguments['compact'].cleanup_commits,
threshold=arguments['compact'].threshold,
)
else:
logger.info(
'{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository)
)
if 'create' in arguments: if 'create' in arguments:
logger.info('{}: Creating archive{}'.format(repository, dry_run_label)) logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
dispatch.call_hooks( dispatch.call_hooks(
@ -339,6 +397,7 @@ def run_actions(
repository, repository,
location, location,
storage, storage,
local_borg_version,
local_path=local_path, local_path=local_path,
remote_path=remote_path, remote_path=remote_path,
progress=arguments['create'].progress, progress=arguments['create'].progress,
@ -378,6 +437,7 @@ def run_actions(
arguments['extract'].paths, arguments['extract'].paths,
location, location,
storage, storage,
local_borg_version,
local_path=local_path, local_path=local_path,
remote_path=remote_path, remote_path=remote_path,
destination_path=arguments['extract'].destination, destination_path=arguments['extract'].destination,
@ -486,6 +546,7 @@ def run_actions(
paths=dump.convert_glob_patterns_to_borg_patterns([dump_pattern]), paths=dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
location_config=location, location_config=location,
storage_config=storage, storage_config=storage,
local_borg_version=local_borg_version,
local_path=local_path, local_path=local_path,
remote_path=remote_path, remote_path=remote_path,
destination_path='/', destination_path='/',
@ -597,6 +658,20 @@ def load_configurations(config_filenames, overrides=None):
configs[config_filename] = validate.parse_configuration( configs[config_filename] = validate.parse_configuration(
config_filename, validate.schema_filename(), overrides config_filename, validate.schema_filename(), overrides
) )
except PermissionError:
logs.extend(
[
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg='{}: Insufficient permissions to read configuration file'.format(
config_filename
),
)
),
]
)
except (ValueError, OSError, validate.Validation_error) as error: except (ValueError, OSError, validate.Validation_error) as error:
logs.extend( logs.extend(
[ [

View File

@ -42,6 +42,14 @@ properties:
example: example:
- user@backupserver:sourcehostname.borg - user@backupserver:sourcehostname.borg
- "user@backupserver:{fqdn}" - "user@backupserver:{fqdn}"
working_directory:
type: string
description: |
Working directory for the "borg create" command. Tildes are
expanded. Useful for backing up using relative paths. See
http://borgbackup.readthedocs.io/en/stable/usage/create.html
for details. Defaults to not set.
example: /path/to/working/directory
one_file_system: one_file_system:
type: boolean type: boolean
description: | description: |
@ -58,7 +66,9 @@ properties:
example: true example: true
atime: atime:
type: boolean type: boolean
description: Store atime into archive. Defaults to true. description: |
Store atime into archive. Defaults to true in Borg < 1.2,
false in Borg 1.2+.
example: false example: false
ctime: ctime:
type: boolean type: boolean
@ -109,10 +119,10 @@ properties:
type: string type: string
description: | description: |
Any paths matching these patterns are included/excluded from Any paths matching these patterns are included/excluded from
backups. Globs are expanded. (Tildes are not.) Note that backups. Globs are expanded. (Tildes are not.) See the
Borg considers this option experimental. See the output of output of "borg help patterns" for more details. Quote any
"borg help patterns" for more details. Quote any value if it value if it contains leading punctuation, so it parses
contains leading punctuation, so it parses correctly. correctly.
example: example:
- 'R /' - 'R /'
- '- /home/*/.cache' - '- /home/*/.cache'
@ -346,23 +356,28 @@ properties:
init: init:
type: string type: string
description: | description: |
Extra command-line options to pass to "borg init". Extra command-line options to pass to "borg init".
example: "--make-parent-dirs" example: "--extra-option"
prune: prune:
type: string type: string
description: | description: |
Extra command-line options to pass to "borg prune". Extra command-line options to pass to "borg prune".
example: "--save-space" example: "--extra-option"
compact:
type: string
description: |
Extra command-line options to pass to "borg compact".
example: "--extra-option"
create: create:
type: string type: string
description: | description: |
Extra command-line options to pass to "borg create". Extra command-line options to pass to "borg create".
example: "--no-files-cache" example: "--extra-option"
check: check:
type: string type: string
description: | description: |
Extra command-line options to pass to "borg check". Extra command-line options to pass to "borg check".
example: "--save-space" example: "--extra-option"
description: | description: |
Additional options to pass directly to particular Borg Additional options to pass directly to particular Borg
commands, handy for Borg options that borgmatic does not yet commands, handy for Borg options that borgmatic does not yet
@ -522,6 +537,15 @@ properties:
before pruning, run once per configuration file. before pruning, run once per configuration file.
example: example:
- echo "Starting pruning." - echo "Starting pruning."
before_compact:
type: array
items:
type: string
description: |
List of one or more shell commands or scripts to execute
before compaction, run once per configuration file.
example:
- echo "Starting compaction."
before_check: before_check:
type: array type: array
items: items:
@ -549,6 +573,15 @@ properties:
after creating a backup, run once per configuration file. after creating a backup, run once per configuration file.
example: example:
- echo "Finished a backup." - echo "Finished a backup."
after_compact:
type: array
items:
type: string
description: |
List of one or more shell commands or scripts to execute
after compaction, run once per configuration file.
example:
- echo "Finished compaction."
after_prune: after_prune:
type: array type: array
items: items:
@ -582,10 +615,11 @@ properties:
type: string type: string
description: | description: |
List of one or more shell commands or scripts to execute List of one or more shell commands or scripts to execute
when an exception occurs during a "prune", "create", or when an exception occurs during a "prune", "compact",
"check" action or an associated before/after hook. "create", or "check" action or an associated before/after
hook.
example: example:
- echo "Error during prune/create/check." - echo "Error during prune/compact/create/check."
before_everything: before_everything:
type: array type: array
items: items:

View File

@ -15,7 +15,7 @@ def schema_filename():
return pkg_resources.resource_filename('borgmatic', 'config/schema.yaml') return pkg_resources.resource_filename('borgmatic', 'config/schema.yaml')
def format_error_path_element(path_element): def format_json_error_path_element(path_element):
''' '''
Given a path element into a JSON data structure, format it for display as a string. Given a path element into a JSON data structure, format it for display as a string.
''' '''
@ -25,14 +25,14 @@ def format_error_path_element(path_element):
return str('.{}'.format(path_element)) return str('.{}'.format(path_element))
def format_error(error): def format_json_error(error):
''' '''
Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string. Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
''' '''
if not error.path: if not error.path:
return 'At the top level: {}'.format(error.message) return 'At the top level: {}'.format(error.message)
formatted_path = ''.join(format_error_path_element(element) for element in error.path) formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
return "At '{}': {}".format(formatted_path.lstrip('.'), error.message) return "At '{}': {}".format(formatted_path.lstrip('.'), error.message)
@ -44,8 +44,8 @@ class Validation_error(ValueError):
def __init__(self, config_filename, errors): def __init__(self, config_filename, errors):
''' '''
Given a configuration filename path and a sequence of Given a configuration filename path and a sequence of string error messages, create a
jsonschema.exceptions.ValidationError instances, create a Validation_error. Validation_error.
''' '''
self.config_filename = config_filename self.config_filename = config_filename
self.errors = errors self.errors = errors
@ -56,7 +56,7 @@ class Validation_error(ValueError):
''' '''
return 'An error occurred while parsing a configuration file at {}:\n'.format( return 'An error occurred while parsing a configuration file at {}:\n'.format(
self.config_filename self.config_filename
) + '\n'.join(format_error(error) for error in self.errors) ) + '\n'.join(error for error in self.errors)
def apply_logical_validation(config_filename, parsed_configuration): def apply_logical_validation(config_filename, parsed_configuration):
@ -117,7 +117,9 @@ def parse_configuration(config_filename, schema_filename, overrides=None):
validation_errors = tuple(validator.iter_errors(config)) validation_errors = tuple(validator.iter_errors(config))
if validation_errors: if validation_errors:
raise Validation_error(config_filename, validation_errors) raise Validation_error(
config_filename, tuple(format_json_error(error) for error in validation_errors)
)
apply_logical_validation(config_filename, config) apply_logical_validation(config_filename, config)

View File

@ -1,9 +1,10 @@
FROM python:3.8-alpine3.12 as borgmatic FROM python:3.8-alpine3.13 as borgmatic
COPY . /app COPY . /app
RUN pip install --no-cache ruamel.yaml.clib==0.2.2 /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml RUN apk add --no-cache 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 \ RUN borgmatic --help > /command-line.txt \
&& for action in init prune create check extract export-tar mount umount restore list info borg; do \ && for action in init prune compact create check extract export-tar mount umount restore list info borg; do \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \ echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic "$action" --help >> /command-line.txt; done && borgmatic "$action" --help >> /command-line.txt; done

View File

@ -258,6 +258,7 @@ footer.elv-layout {
/* Header */ /* Header */
.elv-header { .elv-header {
position: relative; position: relative;
text-align: center;
} }
.elv-header-default { .elv-header-default {
display: flex; display: flex;

View File

@ -33,9 +33,33 @@ configuration file, right before the `create` action. `after_backup` hooks run
afterwards, but not if an error occurs in a previous hook or in the backups afterwards, but not if an error occurs in a previous hook or in the backups
themselves. themselves.
There are additional hooks for the `prune` and `check` actions as well. There are additional hooks that run before/after other actions as well. For
`before_prune` and `after_prune` run if there are any `prune` actions, while instance, `before_prune` runs before a `prune` action, while `after_prune`
`before_check` and `after_check` run if there are any `check` actions. runs after it.
## Variable interpolation
The before and after action hooks support interpolating particular runtime
variables into the hook command. Here's an example that assumes you provide a
separate shell script:
```yaml
hooks:
after_prune:
- record-prune.sh "{configuration_filename}" "{repositories}"
```
In this example, when the hook is triggered, borgmatic interpolates runtime
values into the hook command: the borgmatic configuration filename and the
paths of all configured repositories. Here's the full set of supported
variables you can use here:
* `configuration_filename`: borgmatic configuration filename in which the
hook was defined
* `repositories`: comma-separated paths of all repositories configured in the
current borgmatic configuration file
## Global hooks
You can also use `before_everything` and `after_everything` hooks to perform You can also use `before_everything` and `after_everything` hooks to perform
global setup or cleanup: global setup or cleanup:
@ -58,6 +82,8 @@ but only if there is a `create` action. It runs even if an error occurs during
a backup or a backup hook, but not if an error occurs during a a backup or a backup hook, but not if an error occurs during a
`before_everything` hook. `before_everything` hook.
## Error hooks
borgmatic also runs `on_error` hooks if an error occurs, either when creating borgmatic also runs `on_error` hooks if an error occurs, either when creating
a backup or running a backup hook. See the [monitoring and alerting a backup or running a backup hook. See the [monitoring and alerting
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/) documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)

View File

@ -115,6 +115,6 @@ There are some caveats you should be aware of with this feature.
* The soft failure doesn't have to apply to a repository. You can even perform * The soft failure doesn't have to apply to a repository. You can even perform
a test to make sure that individual source directories are mounted and a test to make sure that individual source directories are mounted and
available. Use your imagination! available. Use your imagination!
* The soft failure feature also works for `before_prune`, `after_prune`, * The soft failure feature also works for before/after hooks for other
`before_check`, and `after_check` hooks. But it is not implemented for actions as well. But it is not implemented for `before_everything` or
`before_everything` or `after_everything`. `after_everything`.

View File

@ -199,10 +199,10 @@ backups to avoid getting caught without a way to restore a database.
databases that share the exact same name on different hosts. databases that share the exact same name on different hosts.
4. Because database hooks implicitly enable the `read_special` configuration 4. Because database hooks implicitly enable the `read_special` configuration
setting to support dump and restore streaming, you'll need to ensure that any setting to support dump and restore streaming, you'll need to ensure that any
special files are excluded from backups (named pipes, block devices, and special files are excluded from backups (named pipes, block devices,
character devices) to prevent hanging. Try a command like `find / -type c,b,p` character devices, and sockets) to prevent hanging. Try a command like
to find such files. Common directories to exclude are `/dev` and `/run`, but `find /your/source/path -type c,b,p,s` to find such files. Common directories
that may not be exhaustive. to exclude are `/dev` and `/run`, but that may not be exhaustive.
### Manual restoration ### Manual restoration
@ -244,5 +244,10 @@ hooks:
### borgmatic hangs during backup ### borgmatic hangs during backup
See Limitations above about `read_special`. You may need to exclude certain See Limitations above about `read_special`. You may need to exclude certain
paths with named pipes, block devices, or character devices on which borgmatic paths with named pipes, block devices, character devices, or sockets on which
is hanging. borgmatic is hanging.
Alternatively, if excluding special files is too onerous, you can create two
separate borgmatic configuration files—one for your source files and a
separate one for backing up databases. That way, the database `read_special`
option will not be active when backing up special files.

View File

@ -9,19 +9,20 @@ eleventyNavigation:
Borg itself is great for efficiently de-duplicating data across successive Borg itself is great for efficiently de-duplicating data across successive
backup archives, even when dealing with very large repositories. But you may backup archives, even when dealing with very large repositories. But you may
find that while borgmatic's default mode of "prune, create, and check" works find that while borgmatic's default mode of `prune`, `compact`, `create`, and
well on small repositories, it's not so great on larger ones. That's because `check` works well on small repositories, it's not so great on larger ones.
running the default pruning and consistency checks take a long time on large That's because running the default pruning, compact, and consistency checks
repositories. take a long time on large repositories.
### A la carte actions ### A la carte actions
If you find yourself in this situation, you have some options. First, you can If you find yourself in this situation, you have some options. First, you can
run borgmatic's pruning, creating, or checking actions separately. For run borgmatic's `prune`, `compact`, `create`, or `check` actions separately.
instance, the following optional actions are available: For instance, the following optional actions are available:
```bash ```bash
borgmatic prune borgmatic prune
borgmatic compact
borgmatic create borgmatic create
borgmatic check borgmatic check
``` ```
@ -32,7 +33,7 @@ borgmatic check
You can run with only one of these actions provided, or you can mix and match 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 any number of them in a single borgmatic run. This supports approaches like
skipping certain actions while running others. For instance, this skips skipping certain actions while running others. For instance, this skips
`prune` and only runs `create` and `check`: `prune` and `compact` and only runs `create` and `check`:
```bash ```bash
borgmatic create check borgmatic create check

View File

@ -83,10 +83,10 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
## Error hooks ## Error hooks
When an error occurs during a `prune`, `create`, or `check` action, borgmatic When an error occurs during a `prune`, `compact`, `create`, or `check` action,
can run configurable shell commands to fire off custom error notifications or borgmatic can run configurable shell commands to fire off custom error
take other actions, so you can get alerted as soon as something goes wrong. notifications or take other actions, so you can get alerted as soon as
Here's a not-so-useful example: something goes wrong. Here's a not-so-useful example:
```yaml ```yaml
hooks: hooks:
@ -104,10 +104,9 @@ hooks:
- send-text-message.sh "{configuration_filename}" "{repository}" - send-text-message.sh "{configuration_filename}" "{repository}"
``` ```
In this example, when the error occurs, borgmatic interpolates a few runtime In this example, when the error occurs, borgmatic interpolates runtime values
values into the hook command: the borgmatic configuration filename, and the into the hook command: the borgmatic configuration filename, and the path of
path of the repository. Here's the full set of supported variables you can use the repository. Here's the full set of supported variables you can use here:
here:
* `configuration_filename`: borgmatic configuration filename in which the * `configuration_filename`: borgmatic configuration filename in which the
error occurred error occurred
@ -117,9 +116,9 @@ here:
* `output`: output of the command that failed (may be blank if an error * `output`: output of the command that failed (may be blank if an error
occurred without running a command) occurred without running a command)
Note that borgmatic runs the `on_error` hooks only for `prune`, `create`, or Note that borgmatic runs the `on_error` hooks only for `prune`, `compact`,
`check` actions or hooks in which an error occurs, and not other actions. `create`, or `check` actions or hooks in which an error occurs, and not other
borgmatic does not run `on_error` hooks if an error occurs within a actions. borgmatic does not run `on_error` hooks if an error occurs within a
`before_everything` or `after_everything` hook. For more about hooks, see the `before_everything` or `after_everything` hook. For more about hooks, see the
[borgmatic hooks [borgmatic hooks
documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/), documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
@ -144,7 +143,7 @@ With this hook in place, borgmatic pings your Healthchecks project when a
backup begins, ends, or errors. Specifically, after the <a backup begins, ends, or errors. Specifically, after the <a
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup` href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of hooks</a> run, borgmatic lets Healthchecks know that it has started if any of
the `prune`, `create`, or `check` actions are run. the `prune`, `compact`, `create`, or `check` actions are run.
Then, if the actions complete successfully, borgmatic notifies Healthchecks of Then, if the actions complete successfully, borgmatic notifies Healthchecks of
the success after the `after_backup` hooks run, and includes borgmatic logs in the success after the `after_backup` hooks run, and includes borgmatic logs in
@ -155,7 +154,7 @@ in the Healthchecks UI, although be aware that Healthchecks currently has a
If an error occurs during any action or hook, borgmatic notifies Healthchecks If an error occurs during any action or hook, borgmatic notifies Healthchecks
after the `on_error` hooks run, also tacking on logs including the error after the `on_error` hooks run, also tacking on logs including the error
itself. But the logs are only included for errors that occur when a `prune`, itself. But the logs are only included for errors that occur when a `prune`,
`create`, or `check` action is run. `compact`, `create`, or `check` action is run.
You can customize the verbosity of the logs that are sent to Healthchecks with You can customize the verbosity of the logs that are sent to Healthchecks with
borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags
@ -184,8 +183,8 @@ With this hook in place, borgmatic pings your Cronitor monitor when a backup
begins, ends, or errors. Specifically, after the <a begins, ends, or errors. Specifically, after the <a
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup` href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
hooks</a> run, borgmatic lets Cronitor know that it has started if any of the hooks</a> run, borgmatic lets Cronitor know that it has started if any of the
`prune`, `create`, or `check` actions are run. Then, if the actions complete `prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
successfully, borgmatic notifies Cronitor of the success after the complete successfully, borgmatic notifies Cronitor of the success after the
`after_backup` hooks run. And if an error occurs during any action or hook, `after_backup` hooks run. And if an error occurs during any action or hook,
borgmatic notifies Cronitor after the `on_error` hooks run. borgmatic notifies Cronitor after the `on_error` hooks run.
@ -212,8 +211,8 @@ With this hook in place, borgmatic pings your Cronhub monitor when a backup
begins, ends, or errors. Specifically, after the <a begins, ends, or errors. Specifically, after the <a
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup` href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
hooks</a> run, borgmatic lets Cronhub know that it has started if any of the hooks</a> run, borgmatic lets Cronhub know that it has started if any of the
`prune`, `create`, or `check` actions are run. Then, if the actions complete `prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
successfully, borgmatic notifies Cronhub of the success after the complete successfully, borgmatic notifies Cronhub of the success after the
`after_backup` hooks run. And if an error occurs during any action or hook, `after_backup` hooks run. And if an error occurs during any action or hook,
borgmatic notifies Cronhub after the `on_error` hooks run. borgmatic notifies Cronhub after the `on_error` hooks run.
@ -252,9 +251,9 @@ hooks:
With this hook in place, borgmatic creates a PagerDuty event for your service With this hook in place, borgmatic creates a PagerDuty event for your service
whenever backups fail. Specifically, if an error occurs during a `create`, whenever backups fail. Specifically, if an error occurs during a `create`,
`prune`, or `check` action, borgmatic sends an event to PagerDuty before the `prune`, `compact`, or `check` action, borgmatic sends an event to PagerDuty
`on_error` hooks run. Note that borgmatic does not contact PagerDuty when a before the `on_error` hooks run. Note that borgmatic does not contact
backup starts or ends without error. PagerDuty when a backup starts or ends without error.
You can configure PagerDuty to notify you by a [variety of You can configure PagerDuty to notify you by a [variety of
mechanisms](https://support.pagerduty.com/docs/notifications) when backups mechanisms](https://support.pagerduty.com/docs/notifications) when backups

View File

@ -227,8 +227,8 @@ sudo borgmatic --verbosity 1 --files
borgmatic. So try leaving it out, or upgrade borgmatic!) borgmatic. So try leaving it out, or upgrade borgmatic!)
By default, this will also prune any old backups as per the configured By default, this will also prune any old backups as per the configured
retention policy, and check backups for consistency problems due to things retention policy, compact segments to free up space (with Borg 1.2+), and
like file damage. check backups for consistency problems due to things like file damage.
The verbosity flag makes borgmatic show the steps it's performing. And the The verbosity flag makes borgmatic show the steps it's performing. And the
files flag lists each file that's new or changed since the last backup. files flag lists each file that's new or changed since the last backup.

View File

@ -43,6 +43,7 @@ ProtectSystem=full
# ProtectHome=tmpfs # ProtectHome=tmpfs
# BindPaths=-/root/.cache/borg -/root/.cache/borg -/root/.borgmatic # BindPaths=-/root/.cache/borg -/root/.cache/borg -/root/.borgmatic
# May interfere with running external programs within borgmatic hooks.
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
# Lower CPU and I/O priority. # Lower CPU and I/O priority.

View File

@ -38,7 +38,7 @@ for sub_command in prune create check list info; do
| grep -v '^--json$' \ | grep -v '^--json$' \
| grep -v '^--keep-last$' \ | grep -v '^--keep-last$' \
| grep -v '^--list$' \ | grep -v '^--list$' \
| grep -v '^--nobsdflags$' \ | grep -v '^--bsdflags$' \
| grep -v '^--pattern$' \ | grep -v '^--pattern$' \
| grep -v '^--progress$' \ | grep -v '^--progress$' \
| grep -v '^--stats$' \ | grep -v '^--stats$' \
@ -54,7 +54,7 @@ for sub_command in prune create check list info; do
| grep -v '^--format' \ | grep -v '^--format' \
| grep -v '^--glob-archives' \ | grep -v '^--glob-archives' \
| grep -v '^--last' \ | grep -v '^--last' \
| grep -v '^--list-format' \ | grep -v '^--format' \
| grep -v '^--patterns-from' \ | grep -v '^--patterns-from' \
| grep -v '^--prefix' \ | grep -v '^--prefix' \
| grep -v '^--short' \ | grep -v '^--short' \

View File

@ -31,8 +31,8 @@ python3 setup.py bdist_wheel
python3 setup.py sdist python3 setup.py sdist
gpg --detach-sign --armor dist/borgmatic-*.tar.gz gpg --detach-sign --armor dist/borgmatic-*.tar.gz
gpg --detach-sign --armor dist/borgmatic-*-py3-none-any.whl gpg --detach-sign --armor dist/borgmatic-*-py3-none-any.whl
twine upload -r pypi dist/borgmatic-*.tar.gz dist/borgmatic-*.tar.gz.asc twine upload -r pypi --username __token__ dist/borgmatic-*.tar.gz dist/borgmatic-*.tar.gz.asc
twine upload -r pypi dist/borgmatic-*-py3-none-any.whl dist/borgmatic-*-py3-none-any.whl.asc twine upload -r pypi --username __token__ dist/borgmatic-*-py3-none-any.whl dist/borgmatic-*-py3-none-any.whl.asc
# Set release changelogs on projects.torsion.org and GitHub. # Set release changelogs on projects.torsion.org and GitHub.
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')" release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"

View File

@ -10,11 +10,12 @@
set -e set -e
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
py3-ruamel.yaml py3-ruamel.yaml.clib
# If certain dependencies of black are available in this version of Alpine, install them. # If certain dependencies of black are available in this version of Alpine, install them.
apk add --no-cache py3-typed-ast py3-regex || true apk add --no-cache py3-typed-ast py3-regex || true
python3 -m pip install --upgrade pip==21.3.1 setuptools==58.2.0 python3 -m pip install --no-cache --upgrade pip==22.0.3 setuptools==60.8.1
pip3 install tox==3.24.4 pip3 install tox==3.24.5
export COVERAGE_FILE=/tmp/.coverage export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox --sitepackages tox --workdir /tmp/.tox --sitepackages
tox --workdir /tmp/.tox --sitepackages -e end-to-end tox --workdir /tmp/.tox --sitepackages -e end-to-end

View File

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.5.22' VERSION = '1.5.24'
setup( setup(
@ -37,4 +37,5 @@ setup(
'colorama>=0.4.1,<0.5', 'colorama>=0.4.1,<0.5',
), ),
include_package_data=True, include_package_data=True,
python_requires='>3.7.0',
) )

View File

@ -4,15 +4,15 @@ black==19.10b0; python_version >= '3.8'
click==7.1.2; python_version >= '3.8' click==7.1.2; python_version >= '3.8'
colorama==0.4.4 colorama==0.4.4
coverage==5.3 coverage==5.3
flake8==3.8.4 flake8==4.0.1
flexmock==0.10.4 flexmock==0.10.4
isort==5.9.1 isort==5.9.1
mccabe==0.6.1 mccabe==0.6.1
pluggy==0.13.1 pluggy==0.13.1
pathspec==0.8.1; python_version >= '3.8' pathspec==0.8.1; python_version >= '3.8'
py==1.10.0 py==1.10.0
pycodestyle==2.6.0 pycodestyle==2.8.0
pyflakes==2.2.0 pyflakes==2.4.0
jsonschema==3.2.0 jsonschema==3.2.0
pytest==6.2.5 pytest==6.2.5
pytest-cov==3.0.0 pytest-cov==3.0.0

View File

@ -0,0 +1,17 @@
from borgmatic.borg import feature as module
def test_available_true_for_new_enough_borg_version():
assert module.available(module.Feature.COMPACT, '1.3.7')
def test_available_true_for_borg_version_introducing_feature():
assert module.available(module.Feature.COMPACT, '1.2.0a2')
def test_available_true_for_borg_stable_version_introducing_feature():
assert module.available(module.Feature.COMPACT, '1.2.0')
def test_available_false_for_too_old_borg_version():
assert not module.available(module.Feature.COMPACT, '1.1.5')

View File

@ -0,0 +1,110 @@
import logging
from flexmock import flexmock
from borgmatic.borg import compact as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(compact_command, output_log_level):
flexmock(module).should_receive('execute_command').with_args(
compact_command, output_log_level=output_log_level, borg_local_path=compact_command[0]
).once()
COMPACT_COMMAND = ('borg', 'compact')
def test_compact_segments_calls_borg_with_parameters():
insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO)
module.compact_segments(dry_run=False, repository='repo', storage_config={})
def test_compact_segments_with_log_info_calls_borg_with_info_parameter():
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)
def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter():
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)
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)
def test_compact_segments_with_local_path_calls_borg_via_local_path():
insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, local_path='borg1',
)
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters():
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',
)
def test_compact_segments_with_progress_calls_borg_with_progress_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, progress=True,
)
def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, cleanup_commits=True,
)
def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, threshold=20,
)
def test_compact_segments_with_umask_calls_borg_with_umask_parameters():
storage_config = {'umask': '077'}
insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
)
def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
)
def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options():
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'}},
)

File diff suppressed because it is too large Load Diff

View File

@ -25,12 +25,14 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n' ('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'
) )
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2')) 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(repository='repo', lock_wait=None) module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
def test_extract_last_archive_dry_run_without_any_archives_should_not_raise(): def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n') insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None) module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -41,6 +43,7 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet
) )
insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2')) insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2'))
insert_logging_mock(logging.INFO) insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None) module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -53,6 +56,7 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param
('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2') ('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2')
) )
insert_logging_mock(logging.DEBUG) insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None) module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -62,6 +66,7 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path():
('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n' ('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'
) )
insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2')) 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(repository='repo', lock_wait=None, local_path='borg1') module.extract_last_archive_dry_run(repository='repo', lock_wait=None, local_path='borg1')
@ -73,6 +78,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
insert_execute_command_mock( insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2') ('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2')
) )
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None, remote_path='borg1') module.extract_last_archive_dry_run(repository='repo', lock_wait=None, remote_path='borg1')
@ -84,6 +90,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
insert_execute_command_mock( insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2') ('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2')
) )
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=5) module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
@ -91,6 +98,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
def test_extract_archive_calls_borg_with_path_parameters(): def test_extract_archive_calls_borg_with_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2')) insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -99,12 +107,14 @@ def test_extract_archive_calls_borg_with_path_parameters():
paths=['path1', 'path2'], paths=['path1', 'path2'],
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )
def test_extract_archive_calls_borg_with_remote_path_parameters(): def test_extract_archive_calls_borg_with_remote_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -113,13 +123,18 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
remote_path='borg1', remote_path='borg1',
) )
def test_extract_archive_calls_borg_with_numeric_owner_parameter(): @pytest.mark.parametrize(
'feature_available,option_flag', ((True, '--numeric-ids'), (False, '--numeric-owner'),),
)
def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available, option_flag):
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--numeric-owner', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(feature_available)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -128,12 +143,14 @@ def test_extract_archive_calls_borg_with_numeric_owner_parameter():
paths=None, paths=None,
location_config={'numeric_owner': True}, location_config={'numeric_owner': True},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )
def test_extract_archive_calls_borg_with_umask_parameters(): def test_extract_archive_calls_borg_with_umask_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -142,12 +159,14 @@ def test_extract_archive_calls_borg_with_umask_parameters():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={'umask': '0770'}, storage_config={'umask': '0770'},
local_borg_version='1.2.3',
) )
def test_extract_archive_calls_borg_with_lock_wait_parameters(): def test_extract_archive_calls_borg_with_lock_wait_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -156,6 +175,7 @@ def test_extract_archive_calls_borg_with_lock_wait_parameters():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={'lock_wait': '5'}, storage_config={'lock_wait': '5'},
local_borg_version='1.2.3',
) )
@ -163,6 +183,7 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO) insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -171,6 +192,7 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )
@ -180,6 +202,7 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
('borg', 'extract', '--debug', '--list', '--show-rc', 'repo::archive') ('borg', 'extract', '--debug', '--list', '--show-rc', 'repo::archive')
) )
insert_logging_mock(logging.DEBUG) insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -188,12 +211,14 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )
def test_extract_archive_calls_borg_with_dry_run_parameter(): def test_extract_archive_calls_borg_with_dry_run_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=True, dry_run=True,
@ -202,12 +227,14 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )
def test_extract_archive_calls_borg_with_destination_path(): def test_extract_archive_calls_borg_with_destination_path():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest') insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest')
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -216,6 +243,7 @@ def test_extract_archive_calls_borg_with_destination_path():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
destination_path='/dest', destination_path='/dest',
) )
@ -223,6 +251,7 @@ def test_extract_archive_calls_borg_with_destination_path():
def test_extract_archive_calls_borg_with_strip_components(): def test_extract_archive_calls_borg_with_strip_components():
flexmock(module.os.path).should_receive('abspath').and_return('repo') flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive')) insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -231,6 +260,7 @@ def test_extract_archive_calls_borg_with_strip_components():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
strip_components=5, strip_components=5,
) )
@ -242,6 +272,7 @@ def test_extract_archive_calls_borg_with_progress_parameter():
output_file=module.DO_NOT_CAPTURE, output_file=module.DO_NOT_CAPTURE,
working_directory=None, working_directory=None,
).once() ).once()
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -250,6 +281,7 @@ def test_extract_archive_calls_borg_with_progress_parameter():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
progress=True, progress=True,
) )
@ -265,6 +297,7 @@ def test_extract_archive_with_progress_and_extract_to_stdout_raises():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
progress=True, progress=True,
extract_to_stdout=True, extract_to_stdout=True,
) )
@ -279,6 +312,7 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
working_directory=None, working_directory=None,
run_to_completion=False, run_to_completion=False,
).and_return(process).once() ).and_return(process).once()
flexmock(module.feature).should_receive('available').and_return(True)
assert ( assert (
module.extract_archive( module.extract_archive(
@ -288,6 +322,7 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
extract_to_stdout=True, extract_to_stdout=True,
) )
== process == process
@ -299,6 +334,7 @@ def test_extract_archive_skips_abspath_for_remote_repository():
flexmock(module).should_receive('execute_command').with_args( flexmock(module).should_receive('execute_command').with_args(
('borg', 'extract', 'server:repo::archive'), working_directory=None ('borg', 'extract', 'server:repo::archive'), working_directory=None
).once() ).once()
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive( module.extract_archive(
dry_run=False, dry_run=False,
@ -307,4 +343,5 @@ def test_extract_archive_skips_abspath_for_remote_repository():
paths=None, paths=None,
location_config={}, location_config={},
storage_config={}, storage_config={},
local_borg_version='1.2.3',
) )

View File

@ -0,0 +1,49 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.borg import version as module
from ..test_verbosity import insert_logging_mock
VERSION = '1.2.3'
def insert_execute_command_mock(command, borg_local_path='borg', version_output=f'borg {VERSION}'):
flexmock(module).should_receive('execute_command').with_args(
command, output_log_level=None, borg_local_path=borg_local_path
).once().and_return(version_output)
def test_local_borg_version_calls_borg_with_required_parameters():
insert_execute_command_mock(('borg', '--version'))
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_log_info_calls_borg_with_info_parameter():
insert_execute_command_mock(('borg', '--version', '--info'))
insert_logging_mock(logging.INFO)
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_log_debug_calls_borg_with_debug_parameters():
insert_execute_command_mock(('borg', '--version', '--debug', '--show-rc'))
insert_logging_mock(logging.DEBUG)
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_local_borg_path_calls_borg_with_it():
insert_execute_command_mock(('borg1', '--version'), borg_local_path='borg1')
assert module.local_borg_version('borg1') == VERSION
def test_local_borg_version_with_invalid_version_raises():
insert_execute_command_mock(('borg', '--version'), version_output='wtf')
with pytest.raises(ValueError):
module.local_borg_version()

View File

@ -72,12 +72,14 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
def test_parse_subparser_arguments_applies_default_subparsers(): def test_parse_subparser_arguments_applies_default_subparsers():
prune_namespace = flexmock() prune_namespace = flexmock()
compact_namespace = flexmock()
create_namespace = flexmock(progress=True) create_namespace = flexmock(progress=True)
check_namespace = flexmock() check_namespace = flexmock()
subparsers = { subparsers = {
'prune': flexmock( 'prune': flexmock(
parse_known_args=lambda arguments: (prune_namespace, ['prune', '--progress']) parse_known_args=lambda arguments: (prune_namespace, ['prune', '--progress'])
), ),
'compact': flexmock(parse_known_args=lambda arguments: (compact_namespace, [])),
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])), 'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])), 'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
'other': flexmock(), 'other': flexmock(),
@ -87,6 +89,7 @@ def test_parse_subparser_arguments_applies_default_subparsers():
assert arguments == { assert arguments == {
'prune': prune_namespace, 'prune': prune_namespace,
'compact': compact_namespace,
'create': create_namespace, 'create': create_namespace,
'check': check_namespace, 'check': check_namespace,
} }

View File

@ -10,6 +10,7 @@ from borgmatic.commands import borgmatic as module
def test_run_configuration_runs_actions_for_each_repository(): def test_run_configuration_runs_actions_for_each_repository():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
expected_results = [flexmock(), flexmock()] expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return( flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
expected_results[1:] expected_results[1:]
@ -22,8 +23,21 @@ def test_run_configuration_runs_actions_for_each_repository():
assert results == expected_results assert results == expected_results
def test_run_configuration_with_invalid_borg_version_errors():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
flexmock(module.command).should_receive('execute_hook').never()
flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').never()
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_calls_hooks_for_prune_action(): def test_run_configuration_calls_hooks_for_prune_action():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice() flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
@ -33,8 +47,20 @@ def test_run_configuration_calls_hooks_for_prune_action():
list(module.run_configuration('test.yaml', config, arguments)) list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_calls_hooks_for_compact_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module).should_receive('run_actions').and_return([])
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'compact': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_executes_and_calls_hooks_for_create_action(): def test_run_configuration_executes_and_calls_hooks_for_create_action():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice() flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
@ -46,6 +72,7 @@ def test_run_configuration_executes_and_calls_hooks_for_create_action():
def test_run_configuration_calls_hooks_for_check_action(): def test_run_configuration_calls_hooks_for_check_action():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice() flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice() flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
@ -57,6 +84,7 @@ def test_run_configuration_calls_hooks_for_check_action():
def test_run_configuration_calls_hooks_for_extract_action(): def test_run_configuration_calls_hooks_for_extract_action():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice() flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').never() flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
@ -68,6 +96,7 @@ def test_run_configuration_calls_hooks_for_extract_action():
def test_run_configuration_does_not_trigger_hooks_for_list_action(): def test_run_configuration_does_not_trigger_hooks_for_list_action():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').never() flexmock(module.command).should_receive('execute_hook').never()
flexmock(module.dispatch).should_receive('call_hooks').never() flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').and_return([]) flexmock(module).should_receive('run_actions').and_return([])
@ -79,6 +108,7 @@ def test_run_configuration_does_not_trigger_hooks_for_list_action():
def test_run_configuration_logs_actions_error(): def test_run_configuration_logs_actions_error():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module.dispatch).should_receive('call_hooks') flexmock(module.dispatch).should_receive('call_hooks')
expected_results = [flexmock()] expected_results = [flexmock()]
@ -94,6 +124,7 @@ def test_run_configuration_logs_actions_error():
def test_run_configuration_logs_pre_hook_error(): def test_run_configuration_logs_pre_hook_error():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None) flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None)
expected_results = [flexmock()] expected_results = [flexmock()]
flexmock(module).should_receive('make_error_log_records').and_return(expected_results) flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
@ -108,6 +139,7 @@ def test_run_configuration_logs_pre_hook_error():
def test_run_configuration_bails_for_pre_hook_soft_failure(): def test_run_configuration_bails_for_pre_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_raise(error).and_return(None) flexmock(module.command).should_receive('execute_hook').and_raise(error).and_return(None)
flexmock(module).should_receive('make_error_log_records').never() flexmock(module).should_receive('make_error_log_records').never()
@ -122,6 +154,7 @@ def test_run_configuration_bails_for_pre_hook_soft_failure():
def test_run_configuration_logs_post_hook_error(): def test_run_configuration_logs_post_hook_error():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise( flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
OSError OSError
).and_return(None) ).and_return(None)
@ -139,6 +172,7 @@ def test_run_configuration_logs_post_hook_error():
def test_run_configuration_bails_for_post_hook_soft_failure(): def test_run_configuration_bails_for_post_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise( flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
error error
@ -156,6 +190,7 @@ def test_run_configuration_bails_for_post_hook_soft_failure():
def test_run_configuration_logs_on_error_hook_error(): def test_run_configuration_logs_on_error_hook_error():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_raise(OSError) flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
expected_results = [flexmock(), flexmock()] expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('make_error_log_records').and_return( flexmock(module).should_receive('make_error_log_records').and_return(
@ -172,6 +207,7 @@ def test_run_configuration_logs_on_error_hook_error():
def test_run_configuration_bails_for_on_error_hook_soft_failure(): def test_run_configuration_bails_for_on_error_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again') error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(error) flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(error)
expected_results = [flexmock()] expected_results = [flexmock()]
@ -188,6 +224,7 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
def test_run_configuration_retries_soft_error(): def test_run_configuration_retries_soft_error():
# Run action first fails, second passes # Run action first fails, second passes
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([]) flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
expected_results = [flexmock()] expected_results = [flexmock()]
@ -201,6 +238,7 @@ def test_run_configuration_retries_soft_error():
def test_run_configuration_retries_hard_error(): def test_run_configuration_retries_hard_error():
# Run action fails twice # Run action fails twice
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2) flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
expected_results = [flexmock(), flexmock()] expected_results = [flexmock(), flexmock()]
@ -219,6 +257,7 @@ def test_run_configuration_retries_hard_error():
def test_run_repos_ordered(): def test_run_repos_ordered():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2) flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
expected_results = [flexmock(), flexmock()] expected_results = [flexmock(), flexmock()]
@ -236,6 +275,7 @@ def test_run_repos_ordered():
def test_run_configuration_retries_round_robbin(): def test_run_configuration_retries_round_robbin():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4) flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
expected_results = [flexmock(), flexmock(), flexmock(), flexmock()] expected_results = [flexmock(), flexmock(), flexmock(), flexmock()]
@ -259,6 +299,7 @@ def test_run_configuration_retries_round_robbin():
def test_run_configuration_retries_one_passes(): def test_run_configuration_retries_one_passes():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return( flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
[] []
@ -281,6 +322,7 @@ def test_run_configuration_retries_one_passes():
def test_run_configuration_retry_wait(): def test_run_configuration_retry_wait():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4) flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
expected_results = [flexmock(), flexmock(), flexmock(), flexmock()] expected_results = [flexmock(), flexmock(), flexmock(), flexmock()]
@ -310,6 +352,7 @@ def test_run_configuration_retry_wait():
def test_run_configuration_retries_timeout_multiple_repos(): def test_run_configuration_retries_timeout_multiple_repos():
flexmock(module.borg_environment).should_receive('initialize') flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook') flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return( flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
[] []
@ -352,6 +395,15 @@ def test_load_configurations_collects_parsed_configurations():
assert logs == [] assert logs == []
def test_load_configurations_logs_warning_for_permission_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(PermissionError)
configs, logs = tuple(module.load_configurations(('test.yaml',)))
assert configs == {}
assert {log.levelno for log in logs} == {logging.WARNING}
def test_load_configurations_logs_critical_for_parse_error(): def test_load_configurations_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError) flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)

View File

@ -4,33 +4,31 @@ from flexmock import flexmock
from borgmatic.config import validate as module from borgmatic.config import validate as module
def test_format_error_path_element_formats_array_index(): def test_format_json_error_path_element_formats_array_index():
module.format_error_path_element(3) == '[3]' module.format_json_error_path_element(3) == '[3]'
def test_format_error_path_element_formats_property(): def test_format_json_error_path_element_formats_property():
module.format_error_path_element('foo') == '.foo' module.format_json_error_path_element('foo') == '.foo'
def test_format_error_formats_error_including_path(): def test_format_json_error_formats_error_including_path():
flexmock(module).format_error_path_element = lambda element: '.{}'.format(element) flexmock(module).format_json_error_path_element = lambda element: '.{}'.format(element)
error = flexmock(message='oops', path=['foo', 'bar']) error = flexmock(message='oops', path=['foo', 'bar'])
assert module.format_error(error) == "At 'foo.bar': oops" assert module.format_json_error(error) == "At 'foo.bar': oops"
def test_format_error_formats_error_without_path(): def test_format_json_error_formats_error_without_path():
flexmock(module).should_receive('format_error_path_element').never() flexmock(module).should_receive('format_json_error_path_element').never()
error = flexmock(message='oops', path=[]) error = flexmock(message='oops', path=[])
assert module.format_error(error) == 'At the top level: oops' assert module.format_json_error(error) == 'At the top level: oops'
def test_validation_error_string_contains_error_messages_and_config_filename(): def test_validation_error_string_contains_errors():
flexmock(module).format_error = lambda error: error.message flexmock(module).format_json_error = lambda error: error.message
error = module.Validation_error( error = module.Validation_error('config.yaml', ('oops', 'uh oh'))
'config.yaml', (flexmock(message='oops', path=None), flexmock(message='uh oh'))
)
result = str(error) result = str(error)
@ -40,7 +38,7 @@ def test_validation_error_string_contains_error_messages_and_config_filename():
def test_apply_logical_validation_raises_if_archive_name_format_present_without_prefix(): def test_apply_logical_validation_raises_if_archive_name_format_present_without_prefix():
flexmock(module).format_error = lambda error: error.message flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error): with pytest.raises(module.Validation_error):
module.apply_logical_validation( module.apply_logical_validation(
@ -53,7 +51,7 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
def test_apply_logical_validation_raises_if_archive_name_format_present_without_retention_prefix(): def test_apply_logical_validation_raises_if_archive_name_format_present_without_retention_prefix():
flexmock(module).format_error = lambda error: error.message flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error): with pytest.raises(module.Validation_error):
module.apply_logical_validation( module.apply_logical_validation(
@ -67,7 +65,7 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories(): def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
flexmock(module).format_error = lambda error: error.message flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error): with pytest.raises(module.Validation_error):
module.apply_logical_validation( module.apply_logical_validation(

View File

@ -13,7 +13,7 @@ whitelist_externals =
passenv = COVERAGE_FILE passenv = COVERAGE_FILE
commands = commands =
pytest {posargs} pytest {posargs}
py38,py39: black --check . py38,py39,py310: black --check .
isort --check-only --settings-path setup.cfg . isort --check-only --settings-path setup.cfg .
flake8 borgmatic tests flake8 borgmatic tests