mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-08-07 00:23:20 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90595e9c18 | ||
|
|
032d4adee3 | ||
|
|
4444219e17 | ||
|
|
56fd78089d | ||
|
|
86dbc00cbe | ||
|
|
c644270599 | ||
|
|
1676a98c51 | ||
|
|
358ed53da0 | ||
|
|
90925c9428 | ||
|
|
cd192a6909 | ||
|
|
7185146481 | ||
|
|
c15e6c5fe5 | ||
|
|
79c2b9df06 | ||
|
|
acd6772148 | ||
|
|
cd91dbd4f7 | ||
|
|
8fc4efff88 | ||
|
|
4bf3e906a1 | ||
|
|
0ca43ef67a | ||
|
|
603e055a39 | ||
|
|
75c04611dc | ||
|
|
881dc9b01e | ||
|
|
8c72e909a7 |
@@ -0,0 +1,2 @@
|
|||||||
|
.git
|
||||||
|
.tox
|
||||||
@@ -1,3 +1,23 @@
|
|||||||
|
1.3.12
|
||||||
|
* Only log to syslog when run from a non-interactive console (e.g. a cron job).
|
||||||
|
* Remove unicode byte order mark from syslog output so it doesn't show up as a literal in rsyslog
|
||||||
|
output. See discussion on #197.
|
||||||
|
|
||||||
|
1.3.11
|
||||||
|
* #193: Pass through several "borg list" and "borg info" flags like --short, --format, --sort-by,
|
||||||
|
--first, --last, etc. via borgmatic command-line flags.
|
||||||
|
* Add borgmatic info --repository and --archive command-line flags to display info for individual
|
||||||
|
repositories or archives.
|
||||||
|
* Support for Borg --noatime, --noctime, and --nobirthtime flags via corresponding options in
|
||||||
|
borgmatic configuration location section.
|
||||||
|
|
||||||
|
1.3.10
|
||||||
|
* #198: Fix for Borg create error output not showing up at borgmatic verbosity level zero.
|
||||||
|
|
||||||
|
1.3.9
|
||||||
|
* #195: Switch to command-line actions as more traditional sub-commands, e.g. "borgmatic create",
|
||||||
|
"borgmatic prune", etc. However, the classic dashed options like "--create" still work!
|
||||||
|
|
||||||
1.3.8
|
1.3.8
|
||||||
* #191: Disable console color via "color" option in borgmatic configuration output section.
|
* #191: Disable console color via "color" option in borgmatic configuration output section.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
title: borgmatic
|
title: borgmatic
|
||||||
permalink: index.html
|
permalink: index.html
|
||||||
---
|
---
|
||||||
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ def create_archive(
|
|||||||
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
|
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
|
||||||
+ (('--one-file-system',) if location_config.get('one_file_system') else ())
|
+ (('--one-file-system',) if location_config.get('one_file_system') else ())
|
||||||
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
|
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
|
||||||
|
+ (('--noatime',) if location_config.get('atime') is False else ())
|
||||||
|
+ (('--noctime',) if location_config.get('ctime') is False else ())
|
||||||
|
+ (('--nobirthtime',) if location_config.get('birthtime') is False else ())
|
||||||
+ (('--read-special',) if location_config.get('read_special') else ())
|
+ (('--read-special',) if location_config.get('read_special') else ())
|
||||||
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
|
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
|
||||||
+ (('--files-cache', files_cache) if files_cache else ())
|
+ (('--files-cache', files_cache) if files_cache else ())
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
|
def make_flags(name, value):
|
||||||
|
'''
|
||||||
|
Given a flag name and its value, return it formatted as Borg-compatible flags.
|
||||||
|
'''
|
||||||
|
if not value:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
flag = '--{}'.format(name.replace('_', '-'))
|
||||||
|
|
||||||
|
if value is True:
|
||||||
|
return (flag,)
|
||||||
|
|
||||||
|
return (flag, str(value))
|
||||||
|
|
||||||
|
|
||||||
|
def make_flags_from_arguments(arguments, excludes=()):
|
||||||
|
'''
|
||||||
|
Given borgmatic command-line arguments as an instance of argparse.Namespace, and optionally a
|
||||||
|
list of named arguments to exclude, generate and return the corresponding Borg command-line
|
||||||
|
flags as a tuple.
|
||||||
|
'''
|
||||||
|
return tuple(
|
||||||
|
itertools.chain.from_iterable(
|
||||||
|
make_flags(name, value=getattr(arguments, name))
|
||||||
|
for name in sorted(vars(arguments))
|
||||||
|
if name not in excludes and not name.startswith('_')
|
||||||
|
)
|
||||||
|
)
|
||||||
+28
-10
@@ -1,26 +1,44 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def display_archives_info(
|
def display_archives_info(
|
||||||
repository, storage_config, local_path='borg', remote_path=None, json=False
|
repository, storage_config, info_arguments, local_path='borg', remote_path=None
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a local or remote repository path, and a storage config dict, display summary information
|
Given a local or remote repository path, a storage config dict, and the arguments to the info
|
||||||
for Borg archives in the repository or return JSON summary information.
|
action, display summary information for Borg archives in the repository or return JSON summary
|
||||||
|
information.
|
||||||
'''
|
'''
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'info', repository)
|
(
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
local_path,
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
'info',
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
'::'.join((repository, info_arguments.archive))
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
if info_arguments.archive
|
||||||
+ (('--json',) if json else ())
|
else repository,
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
('--info',)
|
||||||
|
if logger.getEffectiveLevel() == logging.INFO and not info_arguments.json
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
('--debug', '--show-rc')
|
||||||
|
if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
+ make_flags('remote-path', remote_path)
|
||||||
|
+ make_flags('lock-wait', lock_wait)
|
||||||
|
+ make_flags_from_arguments(info_arguments, excludes=('repository', 'archive'))
|
||||||
)
|
)
|
||||||
|
|
||||||
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
|
return execute_command(
|
||||||
|
full_command, output_log_level=None if info_arguments.json else logging.WARNING
|
||||||
|
)
|
||||||
|
|||||||
+28
-13
@@ -1,27 +1,42 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def list_archives(
|
def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
|
||||||
repository, storage_config, archive=None, local_path='borg', remote_path=None, json=False
|
|
||||||
):
|
|
||||||
'''
|
'''
|
||||||
Given a local or remote repository path and a storage config dict, display the output of listing
|
Given a local or remote repository path, a storage config dict, and the arguments to the list
|
||||||
Borg archives in the repository or return JSON output. Or, if an archive name is given, listing
|
action, display the output of listing Borg archives in the repository or return JSON output. Or,
|
||||||
the files in that archive.
|
if an archive name is given, listing the files in that archive.
|
||||||
'''
|
'''
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'list', '::'.join((repository, archive)) if archive else repository)
|
(
|
||||||
+ (('--remote-path', remote_path) if remote_path else ())
|
local_path,
|
||||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
'list',
|
||||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
'::'.join((repository, list_arguments.archive))
|
||||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
if list_arguments.archive
|
||||||
+ (('--json',) if json else ())
|
else repository,
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
('--info',)
|
||||||
|
if logger.getEffectiveLevel() == logging.INFO and not list_arguments.json
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
+ (
|
||||||
|
('--debug', '--show-rc')
|
||||||
|
if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
+ make_flags('remote-path', remote_path)
|
||||||
|
+ make_flags('lock-wait', lock_wait)
|
||||||
|
+ make_flags_from_arguments(list_arguments, excludes=('repository', 'archive'))
|
||||||
)
|
)
|
||||||
|
|
||||||
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
|
return execute_command(
|
||||||
|
full_command, output_log_level=None if list_arguments.json else logging.WARNING
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
import collections
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from borgmatic.config import collect
|
||||||
|
|
||||||
|
SUBPARSER_ALIASES = {
|
||||||
|
'init': ['--init', '-I'],
|
||||||
|
'prune': ['--prune', '-p'],
|
||||||
|
'create': ['--create', '-C'],
|
||||||
|
'check': ['--check', '-k'],
|
||||||
|
'extract': ['--extract', '-x'],
|
||||||
|
'list': ['--list', '-l'],
|
||||||
|
'info': ['--info', '-i'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
|
||||||
|
'''
|
||||||
|
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
|
||||||
|
object as returned by argparse.ArgumentParser().add_subparsers(), ask each subparser to parse
|
||||||
|
its own arguments and the top-level parser to parse any remaining arguments.
|
||||||
|
|
||||||
|
Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
|
||||||
|
arguments.
|
||||||
|
'''
|
||||||
|
arguments = collections.OrderedDict()
|
||||||
|
remaining_arguments = list(unparsed_arguments)
|
||||||
|
alias_to_subparser_name = {
|
||||||
|
alias: subparser_name
|
||||||
|
for subparser_name, aliases in SUBPARSER_ALIASES.items()
|
||||||
|
for alias in aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
# Give each requested action's subparser a shot at parsing all arguments.
|
||||||
|
for subparser_name, subparser in subparsers.choices.items():
|
||||||
|
if subparser_name not in unparsed_arguments:
|
||||||
|
continue
|
||||||
|
|
||||||
|
remaining_arguments.remove(subparser_name)
|
||||||
|
canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
|
||||||
|
|
||||||
|
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
|
||||||
|
arguments[canonical_name] = parsed
|
||||||
|
|
||||||
|
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
||||||
|
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
|
||||||
|
for subparser_name in ('prune', 'create', 'check'):
|
||||||
|
subparser = subparsers.choices[subparser_name]
|
||||||
|
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
|
||||||
|
arguments[subparser_name] = parsed
|
||||||
|
|
||||||
|
# Then ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
|
||||||
|
# are global arguments.
|
||||||
|
for subparser_name in arguments.keys():
|
||||||
|
subparser = subparsers.choices[subparser_name]
|
||||||
|
parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||||
|
|
||||||
|
arguments['global'] = top_level_parser.parse_args(remaining_arguments)
|
||||||
|
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
def parse_arguments(*unparsed_arguments):
|
||||||
|
'''
|
||||||
|
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||||
|
them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
|
||||||
|
'''
|
||||||
|
config_paths = collect.get_default_config_paths()
|
||||||
|
|
||||||
|
global_parser = ArgumentParser(add_help=False)
|
||||||
|
global_group = global_parser.add_argument_group('global arguments')
|
||||||
|
|
||||||
|
global_group.add_argument(
|
||||||
|
'-c',
|
||||||
|
'--config',
|
||||||
|
nargs='*',
|
||||||
|
dest='config_paths',
|
||||||
|
default=config_paths,
|
||||||
|
help='Configuration filenames or directories, defaults to: {}'.format(
|
||||||
|
' '.join(config_paths)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'--excludes',
|
||||||
|
dest='excludes_filename',
|
||||||
|
help='Deprecated in favor of exclude_patterns within configuration',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--dry-run',
|
||||||
|
dest='dry_run',
|
||||||
|
action='store_true',
|
||||||
|
help='Go through the motions, but do not actually write to any repositories',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'-v',
|
||||||
|
'--verbosity',
|
||||||
|
type=int,
|
||||||
|
choices=range(0, 3),
|
||||||
|
default=0,
|
||||||
|
help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'--syslog-verbosity',
|
||||||
|
type=int,
|
||||||
|
choices=range(0, 3),
|
||||||
|
default=0,
|
||||||
|
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
|
||||||
|
)
|
||||||
|
global_group.add_argument(
|
||||||
|
'--version',
|
||||||
|
dest='version',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display installed version number of borgmatic and exit',
|
||||||
|
)
|
||||||
|
|
||||||
|
top_level_parser = ArgumentParser(
|
||||||
|
description='''
|
||||||
|
A simple wrapper script for the Borg backup software that creates and prunes backups.
|
||||||
|
If none of the action options are given, then borgmatic defaults to: prune, create, and
|
||||||
|
check archives.
|
||||||
|
''',
|
||||||
|
parents=[global_parser],
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = top_level_parser.add_subparsers(
|
||||||
|
title='actions',
|
||||||
|
metavar='',
|
||||||
|
help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:',
|
||||||
|
)
|
||||||
|
init_parser = subparsers.add_parser(
|
||||||
|
'init',
|
||||||
|
aliases=SUBPARSER_ALIASES['init'],
|
||||||
|
help='Initialize an empty Borg repository',
|
||||||
|
description='Initialize an empty Borg repository',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
init_group = init_parser.add_argument_group('init arguments')
|
||||||
|
init_group.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--encryption',
|
||||||
|
dest='encryption_mode',
|
||||||
|
help='Borg repository encryption mode',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
init_group.add_argument(
|
||||||
|
'--append-only',
|
||||||
|
dest='append_only',
|
||||||
|
action='store_true',
|
||||||
|
help='Create an append-only repository',
|
||||||
|
)
|
||||||
|
init_group.add_argument(
|
||||||
|
'--storage-quota',
|
||||||
|
dest='storage_quota',
|
||||||
|
help='Create a repository with a fixed storage quota',
|
||||||
|
)
|
||||||
|
init_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
prune_parser = subparsers.add_parser(
|
||||||
|
'prune',
|
||||||
|
aliases=SUBPARSER_ALIASES['prune'],
|
||||||
|
help='Prune archives according to the retention policy',
|
||||||
|
description='Prune archives according to the retention policy',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
prune_group = prune_parser.add_argument_group('prune arguments')
|
||||||
|
prune_group.add_argument(
|
||||||
|
'--stats',
|
||||||
|
dest='stats',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display statistics of archive',
|
||||||
|
)
|
||||||
|
prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
create_parser = subparsers.add_parser(
|
||||||
|
'create',
|
||||||
|
aliases=SUBPARSER_ALIASES['create'],
|
||||||
|
help='Create archives (actually perform backups)',
|
||||||
|
description='Create archives (actually perform backups)',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
create_group = create_parser.add_argument_group('create arguments')
|
||||||
|
create_group.add_argument(
|
||||||
|
'--progress',
|
||||||
|
dest='progress',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display progress for each file as it is processed',
|
||||||
|
)
|
||||||
|
create_group.add_argument(
|
||||||
|
'--stats',
|
||||||
|
dest='stats',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display statistics of archive',
|
||||||
|
)
|
||||||
|
create_group.add_argument(
|
||||||
|
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||||
|
)
|
||||||
|
create_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
check_parser = subparsers.add_parser(
|
||||||
|
'check',
|
||||||
|
aliases=SUBPARSER_ALIASES['check'],
|
||||||
|
help='Check archives for consistency',
|
||||||
|
description='Check archives for consistency',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
check_group = check_parser.add_argument_group('check arguments')
|
||||||
|
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
extract_parser = subparsers.add_parser(
|
||||||
|
'extract',
|
||||||
|
aliases=SUBPARSER_ALIASES['extract'],
|
||||||
|
help='Extract a named archive to the current directory',
|
||||||
|
description='Extract a named archive to the current directory',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
extract_group = extract_parser.add_argument_group('extract arguments')
|
||||||
|
extract_group.add_argument(
|
||||||
|
'--repository',
|
||||||
|
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
||||||
|
)
|
||||||
|
extract_group.add_argument('--archive', help='Name of archive to operate on', required=True)
|
||||||
|
extract_group.add_argument(
|
||||||
|
'--restore-path',
|
||||||
|
nargs='+',
|
||||||
|
dest='restore_paths',
|
||||||
|
help='Paths to restore from archive, defaults to the entire archive',
|
||||||
|
)
|
||||||
|
extract_group.add_argument(
|
||||||
|
'--progress',
|
||||||
|
dest='progress',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Display progress for each file as it is processed',
|
||||||
|
)
|
||||||
|
extract_group.add_argument(
|
||||||
|
'-h', '--help', action='help', help='Show this help message and exit'
|
||||||
|
)
|
||||||
|
|
||||||
|
list_parser = subparsers.add_parser(
|
||||||
|
'list',
|
||||||
|
aliases=SUBPARSER_ALIASES['list'],
|
||||||
|
help='List archives',
|
||||||
|
description='List archives or the contents of an archive',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
list_group = list_parser.add_argument_group('list arguments')
|
||||||
|
list_group.add_argument(
|
||||||
|
'--repository',
|
||||||
|
help='Path of repository to list, defaults to the configured repository if there is only one',
|
||||||
|
)
|
||||||
|
list_group.add_argument('--archive', help='Name of archive to list')
|
||||||
|
list_group.add_argument(
|
||||||
|
'--short', default=False, action='store_true', help='Output only archive or path names'
|
||||||
|
)
|
||||||
|
list_group.add_argument('--format', help='Format for file listing')
|
||||||
|
list_group.add_argument(
|
||||||
|
'--json', default=False, action='store_true', help='Output results as JSON'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'-P', '--prefix', help='Only list archive names starting with this prefix'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'--first', metavar='N', help='List first N archives after other filters are applied'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'--last', metavar='N', help='List first N archives after other filters are applied'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
|
||||||
|
)
|
||||||
|
list_group.add_argument(
|
||||||
|
'--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
|
||||||
|
)
|
||||||
|
list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
|
||||||
|
list_group.add_argument(
|
||||||
|
'--pattern-from',
|
||||||
|
metavar='FILENAME',
|
||||||
|
help='Include or exclude paths matching patterns from pattern file, one per line',
|
||||||
|
)
|
||||||
|
list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
info_parser = subparsers.add_parser(
|
||||||
|
'info',
|
||||||
|
aliases=SUBPARSER_ALIASES['info'],
|
||||||
|
help='Display summary information on archives',
|
||||||
|
description='Display summary information on archives',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
info_group = info_parser.add_argument_group('info arguments')
|
||||||
|
info_group.add_argument(
|
||||||
|
'--repository',
|
||||||
|
help='Path of repository to show info for, defaults to the configured repository if there is only one',
|
||||||
|
)
|
||||||
|
info_group.add_argument('--archive', help='Name of archive to show info for')
|
||||||
|
info_group.add_argument(
|
||||||
|
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||||
|
)
|
||||||
|
info_group.add_argument(
|
||||||
|
'-P', '--prefix', help='Only show info for archive names starting with this prefix'
|
||||||
|
)
|
||||||
|
info_group.add_argument(
|
||||||
|
'-a',
|
||||||
|
'--glob-archives',
|
||||||
|
metavar='GLOB',
|
||||||
|
help='Only show info for archive names matching this glob',
|
||||||
|
)
|
||||||
|
info_group.add_argument(
|
||||||
|
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||||
|
)
|
||||||
|
info_group.add_argument(
|
||||||
|
'--first',
|
||||||
|
metavar='N',
|
||||||
|
help='Show info for first N archives after other filters are applied',
|
||||||
|
)
|
||||||
|
info_group.add_argument(
|
||||||
|
'--last', metavar='N', help='Show info for first N archives after other filters are applied'
|
||||||
|
)
|
||||||
|
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers)
|
||||||
|
|
||||||
|
if arguments['global'].excludes_filename:
|
||||||
|
raise ValueError(
|
||||||
|
'The --excludes option has been replaced with exclude_patterns in configuration'
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'init' in arguments and arguments['global'].dry_run:
|
||||||
|
raise ValueError('The init action cannot be used with the --dry-run option')
|
||||||
|
|
||||||
|
if (
|
||||||
|
'list' in arguments
|
||||||
|
and 'info' in arguments
|
||||||
|
and arguments['list'].json
|
||||||
|
and arguments['info'].json
|
||||||
|
):
|
||||||
|
raise ValueError('With the --json option, list and info actions cannot be used together')
|
||||||
|
|
||||||
|
return arguments
|
||||||
+103
-290
@@ -3,7 +3,6 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
@@ -18,6 +17,7 @@ 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 prune as borg_prune
|
from borgmatic.borg import prune as borg_prune
|
||||||
|
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.logger import configure_logging, should_do_markup
|
from borgmatic.logger import configure_logging, should_do_markup
|
||||||
from borgmatic.signals import configure_signals
|
from borgmatic.signals import configure_signals
|
||||||
@@ -28,243 +28,10 @@ logger = logging.getLogger(__name__)
|
|||||||
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments(*arguments):
|
def run_configuration(config_filename, config, arguments): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
||||||
them as an argparse.ArgumentParser instance.
|
dict from subparser name to a namespace of parsed arguments, execute its defined pruning,
|
||||||
'''
|
|
||||||
config_paths = collect.get_default_config_paths()
|
|
||||||
|
|
||||||
parser = ArgumentParser(
|
|
||||||
description='''
|
|
||||||
A simple wrapper script for the Borg backup software that creates and prunes backups.
|
|
||||||
If none of the action options are given, then borgmatic defaults to: prune, create, and
|
|
||||||
check archives.
|
|
||||||
''',
|
|
||||||
add_help=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
actions_group = parser.add_argument_group('actions')
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-I', '--init', dest='init', action='store_true', help='Initialize an empty Borg repository'
|
|
||||||
)
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-p',
|
|
||||||
'--prune',
|
|
||||||
dest='prune',
|
|
||||||
action='store_true',
|
|
||||||
help='Prune archives according to the retention policy',
|
|
||||||
)
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-C',
|
|
||||||
'--create',
|
|
||||||
dest='create',
|
|
||||||
action='store_true',
|
|
||||||
help='Create archives (actually perform backups)',
|
|
||||||
)
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-k', '--check', dest='check', action='store_true', help='Check archives for consistency'
|
|
||||||
)
|
|
||||||
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-x',
|
|
||||||
'--extract',
|
|
||||||
dest='extract',
|
|
||||||
action='store_true',
|
|
||||||
help='Extract a named archive to the current directory',
|
|
||||||
)
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-l', '--list', dest='list', action='store_true', help='List archives'
|
|
||||||
)
|
|
||||||
actions_group.add_argument(
|
|
||||||
'-i',
|
|
||||||
'--info',
|
|
||||||
dest='info',
|
|
||||||
action='store_true',
|
|
||||||
help='Display summary information on archives',
|
|
||||||
)
|
|
||||||
|
|
||||||
init_group = parser.add_argument_group('options for --init')
|
|
||||||
init_group.add_argument(
|
|
||||||
'-e', '--encryption', dest='encryption_mode', help='Borg repository encryption mode'
|
|
||||||
)
|
|
||||||
init_group.add_argument(
|
|
||||||
'--append-only',
|
|
||||||
dest='append_only',
|
|
||||||
action='store_true',
|
|
||||||
help='Create an append-only repository',
|
|
||||||
)
|
|
||||||
init_group.add_argument(
|
|
||||||
'--storage-quota',
|
|
||||||
dest='storage_quota',
|
|
||||||
help='Create a repository with a fixed storage quota',
|
|
||||||
)
|
|
||||||
|
|
||||||
prune_group = parser.add_argument_group('options for --prune')
|
|
||||||
stats_argument = prune_group.add_argument(
|
|
||||||
'--stats',
|
|
||||||
dest='stats',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Display statistics of archive',
|
|
||||||
)
|
|
||||||
|
|
||||||
create_group = parser.add_argument_group('options for --create')
|
|
||||||
progress_argument = create_group.add_argument(
|
|
||||||
'--progress',
|
|
||||||
dest='progress',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Display progress for each file as it is processed',
|
|
||||||
)
|
|
||||||
create_group._group_actions.append(stats_argument)
|
|
||||||
json_argument = create_group.add_argument(
|
|
||||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
|
||||||
)
|
|
||||||
|
|
||||||
extract_group = parser.add_argument_group('options for --extract')
|
|
||||||
repository_argument = extract_group.add_argument(
|
|
||||||
'--repository',
|
|
||||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
|
||||||
)
|
|
||||||
archive_argument = extract_group.add_argument('--archive', help='Name of archive to operate on')
|
|
||||||
extract_group.add_argument(
|
|
||||||
'--restore-path',
|
|
||||||
nargs='+',
|
|
||||||
dest='restore_paths',
|
|
||||||
help='Paths to restore from archive, defaults to the entire archive',
|
|
||||||
)
|
|
||||||
extract_group._group_actions.append(progress_argument)
|
|
||||||
|
|
||||||
list_group = parser.add_argument_group('options for --list')
|
|
||||||
list_group._group_actions.append(repository_argument)
|
|
||||||
list_group._group_actions.append(archive_argument)
|
|
||||||
list_group._group_actions.append(json_argument)
|
|
||||||
|
|
||||||
info_group = parser.add_argument_group('options for --info')
|
|
||||||
info_group._group_actions.append(json_argument)
|
|
||||||
|
|
||||||
common_group = parser.add_argument_group('common options')
|
|
||||||
common_group.add_argument(
|
|
||||||
'-c',
|
|
||||||
'--config',
|
|
||||||
nargs='+',
|
|
||||||
dest='config_paths',
|
|
||||||
default=config_paths,
|
|
||||||
help='Configuration filenames or directories, defaults to: {}'.format(
|
|
||||||
' '.join(config_paths)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'--excludes',
|
|
||||||
dest='excludes_filename',
|
|
||||||
help='Deprecated in favor of exclude_patterns within configuration',
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'-n',
|
|
||||||
'--dry-run',
|
|
||||||
dest='dry_run',
|
|
||||||
action='store_true',
|
|
||||||
help='Go through the motions, but do not actually write to any repositories',
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'-v',
|
|
||||||
'--verbosity',
|
|
||||||
type=int,
|
|
||||||
choices=range(0, 3),
|
|
||||||
default=0,
|
|
||||||
help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'--syslog-verbosity',
|
|
||||||
type=int,
|
|
||||||
choices=range(0, 3),
|
|
||||||
default=0,
|
|
||||||
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)',
|
|
||||||
)
|
|
||||||
common_group.add_argument(
|
|
||||||
'--version',
|
|
||||||
dest='version',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Display installed version number of borgmatic and exit',
|
|
||||||
)
|
|
||||||
common_group.add_argument('--help', action='help', help='Show this help information and exit')
|
|
||||||
|
|
||||||
args = parser.parse_args(arguments)
|
|
||||||
|
|
||||||
if args.excludes_filename:
|
|
||||||
raise ValueError(
|
|
||||||
'The --excludes option has been replaced with exclude_patterns in configuration'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (args.encryption_mode or args.append_only or args.storage_quota) and not args.init:
|
|
||||||
raise ValueError(
|
|
||||||
'The --encryption, --append-only, and --storage-quota options can only be used with the --init option'
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.init and args.dry_run:
|
|
||||||
raise ValueError('The --init option cannot be used with the --dry-run option')
|
|
||||||
if args.init and not args.encryption_mode:
|
|
||||||
raise ValueError('The --encryption option is required with the --init option')
|
|
||||||
|
|
||||||
if not args.extract:
|
|
||||||
if not args.list:
|
|
||||||
if args.repository:
|
|
||||||
raise ValueError(
|
|
||||||
'The --repository option can only be used with the --extract and --list options'
|
|
||||||
)
|
|
||||||
if args.archive:
|
|
||||||
raise ValueError(
|
|
||||||
'The --archive option can only be used with the --extract and --list options'
|
|
||||||
)
|
|
||||||
if args.restore_paths:
|
|
||||||
raise ValueError('The --restore-path option can only be used with the --extract option')
|
|
||||||
if args.extract and not args.archive:
|
|
||||||
raise ValueError('The --archive option is required with the --extract option')
|
|
||||||
|
|
||||||
if args.progress and not (args.create or args.extract):
|
|
||||||
raise ValueError(
|
|
||||||
'The --progress option can only be used with the --create and --extract options'
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.json and not (args.create or args.list or args.info):
|
|
||||||
raise ValueError(
|
|
||||||
'The --json option can only be used with the --create, --list, or --info options'
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.json and args.list and args.info:
|
|
||||||
raise ValueError(
|
|
||||||
'With the --json option, options --list and --info cannot be used together'
|
|
||||||
)
|
|
||||||
|
|
||||||
# If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume
|
|
||||||
# defaults: Mutate the given arguments to enable the default actions.
|
|
||||||
if (
|
|
||||||
not args.init
|
|
||||||
and not args.prune
|
|
||||||
and not args.create
|
|
||||||
and not args.check
|
|
||||||
and not args.extract
|
|
||||||
and not args.list
|
|
||||||
and not args.info
|
|
||||||
):
|
|
||||||
args.prune = True
|
|
||||||
args.create = True
|
|
||||||
args.check = True
|
|
||||||
|
|
||||||
if args.stats and not (args.create or args.prune):
|
|
||||||
raise ValueError('The --stats option can only be used when creating or pruning archives')
|
|
||||||
|
|
||||||
return args
|
|
||||||
|
|
||||||
|
|
||||||
def run_configuration(config_filename, config, args): # pragma: no cover
|
|
||||||
'''
|
|
||||||
Given a config filename and the corresponding parsed config dict, execute its defined pruning,
|
|
||||||
backups, consistency checks, and/or other actions.
|
backups, consistency checks, and/or other actions.
|
||||||
|
|
||||||
Yield JSON output strings from executing any actions that produce JSON.
|
Yield JSON output strings from executing any actions that produce JSON.
|
||||||
@@ -273,24 +40,25 @@ def run_configuration(config_filename, config, args): # pragma: no cover
|
|||||||
config.get(section_name, {})
|
config.get(section_name, {})
|
||||||
for section_name in ('location', 'storage', 'retention', 'consistency', 'hooks')
|
for section_name in ('location', 'storage', 'retention', 'consistency', 'hooks')
|
||||||
)
|
)
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
local_path = location.get('local_path', 'borg')
|
local_path = location.get('local_path', 'borg')
|
||||||
remote_path = location.get('remote_path')
|
remote_path = location.get('remote_path')
|
||||||
borg_environment.initialize(storage)
|
borg_environment.initialize(storage)
|
||||||
|
|
||||||
if args.create:
|
if 'create' in arguments:
|
||||||
hook.execute_hook(
|
hook.execute_hook(
|
||||||
hooks.get('before_backup'),
|
hooks.get('before_backup'),
|
||||||
hooks.get('umask'),
|
hooks.get('umask'),
|
||||||
config_filename,
|
config_filename,
|
||||||
'pre-backup',
|
'pre-backup',
|
||||||
args.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
for repository_path in location['repositories']:
|
for repository_path in location['repositories']:
|
||||||
yield from run_actions(
|
yield from run_actions(
|
||||||
args=args,
|
arguments=arguments,
|
||||||
location=location,
|
location=location,
|
||||||
storage=storage,
|
storage=storage,
|
||||||
retention=retention,
|
retention=retention,
|
||||||
@@ -300,23 +68,35 @@ def run_configuration(config_filename, config, args): # pragma: no cover
|
|||||||
repository_path=repository_path,
|
repository_path=repository_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.create:
|
if 'create' in arguments:
|
||||||
hook.execute_hook(
|
hook.execute_hook(
|
||||||
hooks.get('after_backup'),
|
hooks.get('after_backup'),
|
||||||
hooks.get('umask'),
|
hooks.get('umask'),
|
||||||
config_filename,
|
config_filename,
|
||||||
'post-backup',
|
'post-backup',
|
||||||
args.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
except (OSError, CalledProcessError):
|
except (OSError, CalledProcessError):
|
||||||
hook.execute_hook(
|
hook.execute_hook(
|
||||||
hooks.get('on_error'), hooks.get('umask'), config_filename, 'on-error', args.dry_run
|
hooks.get('on_error'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'on-error',
|
||||||
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def run_actions(
|
def run_actions(
|
||||||
*, args, location, storage, retention, consistency, local_path, remote_path, repository_path
|
*,
|
||||||
|
arguments,
|
||||||
|
location,
|
||||||
|
storage,
|
||||||
|
retention,
|
||||||
|
consistency,
|
||||||
|
local_path,
|
||||||
|
remote_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
|
||||||
@@ -326,82 +106,89 @@ def run_actions(
|
|||||||
Yield JSON output strings from executing any actions that produce JSON.
|
Yield JSON output strings from executing any actions that produce JSON.
|
||||||
'''
|
'''
|
||||||
repository = os.path.expanduser(repository_path)
|
repository = os.path.expanduser(repository_path)
|
||||||
dry_run_label = ' (dry run; not making any changes)' if args.dry_run else ''
|
global_arguments = arguments['global']
|
||||||
if args.init:
|
dry_run_label = ' (dry run; not making any changes)' if global_arguments.dry_run else ''
|
||||||
|
if 'init' in arguments:
|
||||||
logger.info('{}: Initializing repository'.format(repository))
|
logger.info('{}: Initializing repository'.format(repository))
|
||||||
borg_init.initialize_repository(
|
borg_init.initialize_repository(
|
||||||
repository,
|
repository,
|
||||||
args.encryption_mode,
|
arguments['init'].encryption_mode,
|
||||||
args.append_only,
|
arguments['init'].append_only,
|
||||||
args.storage_quota,
|
arguments['init'].storage_quota,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
)
|
)
|
||||||
if args.prune:
|
if 'prune' in arguments:
|
||||||
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
||||||
borg_prune.prune_archives(
|
borg_prune.prune_archives(
|
||||||
args.dry_run,
|
global_arguments.dry_run,
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
retention,
|
retention,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
stats=args.stats,
|
stats=arguments['prune'].stats,
|
||||||
)
|
)
|
||||||
if args.create:
|
if 'create' in arguments:
|
||||||
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
||||||
json_output = borg_create.create_archive(
|
json_output = borg_create.create_archive(
|
||||||
args.dry_run,
|
global_arguments.dry_run,
|
||||||
repository,
|
repository,
|
||||||
location,
|
location,
|
||||||
storage,
|
storage,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
progress=args.progress,
|
progress=arguments['create'].progress,
|
||||||
stats=args.stats,
|
stats=arguments['create'].stats,
|
||||||
json=args.json,
|
json=arguments['create'].json,
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield json.loads(json_output)
|
yield json.loads(json_output)
|
||||||
if args.check and checks.repository_enabled_for_checks(repository, consistency):
|
if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency):
|
||||||
logger.info('{}: Running consistency checks'.format(repository))
|
logger.info('{}: Running consistency checks'.format(repository))
|
||||||
borg_check.check_archives(
|
borg_check.check_archives(
|
||||||
repository, storage, consistency, local_path=local_path, remote_path=remote_path
|
repository, storage, consistency, local_path=local_path, remote_path=remote_path
|
||||||
)
|
)
|
||||||
if args.extract:
|
if 'extract' in arguments:
|
||||||
if args.repository is None or repository == args.repository:
|
if arguments['extract'].repository is None or repository == arguments['extract'].repository:
|
||||||
logger.info('{}: Extracting archive {}'.format(repository, args.archive))
|
logger.info(
|
||||||
|
'{}: Extracting archive {}'.format(repository, arguments['extract'].archive)
|
||||||
|
)
|
||||||
borg_extract.extract_archive(
|
borg_extract.extract_archive(
|
||||||
args.dry_run,
|
global_arguments.dry_run,
|
||||||
repository,
|
repository,
|
||||||
args.archive,
|
arguments['extract'].archive,
|
||||||
args.restore_paths,
|
arguments['extract'].restore_paths,
|
||||||
location,
|
location,
|
||||||
storage,
|
storage,
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
progress=args.progress,
|
progress=arguments['extract'].progress,
|
||||||
)
|
)
|
||||||
if args.list:
|
if 'list' in arguments:
|
||||||
if args.repository is None or repository == args.repository:
|
if arguments['list'].repository is None or repository == arguments['list'].repository:
|
||||||
logger.info('{}: Listing archives'.format(repository))
|
logger.info('{}: Listing archives'.format(repository))
|
||||||
json_output = borg_list.list_archives(
|
json_output = borg_list.list_archives(
|
||||||
repository,
|
repository,
|
||||||
storage,
|
storage,
|
||||||
args.archive,
|
list_arguments=arguments['list'],
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
if json_output:
|
||||||
|
yield json.loads(json_output)
|
||||||
|
if 'info' in arguments:
|
||||||
|
if arguments['info'].repository is None or repository == arguments['info'].repository:
|
||||||
|
logger.info('{}: Displaying summary info for archives'.format(repository))
|
||||||
|
json_output = borg_info.display_archives_info(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
info_arguments=arguments['info'],
|
||||||
local_path=local_path,
|
local_path=local_path,
|
||||||
remote_path=remote_path,
|
remote_path=remote_path,
|
||||||
json=args.json,
|
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield json.loads(json_output)
|
yield json.loads(json_output)
|
||||||
if args.info:
|
|
||||||
logger.info('{}: Displaying summary info for archives'.format(repository))
|
|
||||||
json_output = borg_info.display_archives_info(
|
|
||||||
repository, storage, local_path=local_path, remote_path=remote_path, json=args.json
|
|
||||||
)
|
|
||||||
if json_output:
|
|
||||||
yield json.loads(json_output)
|
|
||||||
|
|
||||||
|
|
||||||
def load_configurations(config_filenames):
|
def load_configurations(config_filenames):
|
||||||
@@ -439,19 +226,27 @@ def load_configurations(config_filenames):
|
|||||||
return (configs, logs)
|
return (configs, logs)
|
||||||
|
|
||||||
|
|
||||||
def collect_configuration_run_summary_logs(configs, args):
|
def collect_configuration_run_summary_logs(configs, arguments):
|
||||||
'''
|
'''
|
||||||
Given a dict of configuration filename to corresponding parsed configuration, and parsed
|
Given a dict of configuration filename to corresponding parsed configuration, and parsed
|
||||||
command-line arguments as an argparse.ArgumentParser instance, run each configuration file and
|
command-line arguments as a dict from subparser name to a parsed namespace of arguments, run
|
||||||
yield a series of logging.LogRecord instances containing summary information about each run.
|
each configuration file and yield a series of logging.LogRecord instances containing summary
|
||||||
|
information about each run.
|
||||||
|
|
||||||
As a side effect of running through these configuration files, output their JSON results, if
|
As a side effect of running through these configuration files, output their JSON results, if
|
||||||
any, to stdout.
|
any, to stdout.
|
||||||
'''
|
'''
|
||||||
# Run cross-file validation checks.
|
# Run cross-file validation checks.
|
||||||
if args.extract or (args.list and args.archive):
|
if 'extract' in arguments:
|
||||||
|
repository = arguments['extract'].repository
|
||||||
|
elif 'list' in arguments and arguments['list'].archive:
|
||||||
|
repository = arguments['list'].repository
|
||||||
|
else:
|
||||||
|
repository = None
|
||||||
|
|
||||||
|
if repository:
|
||||||
try:
|
try:
|
||||||
validate.guard_configuration_contains_repository(args.repository, configs)
|
validate.guard_configuration_contains_repository(repository, configs)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
yield logging.makeLogRecord(
|
yield logging.makeLogRecord(
|
||||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
||||||
@@ -462,7 +257,7 @@ def collect_configuration_run_summary_logs(configs, args):
|
|||||||
json_results = []
|
json_results = []
|
||||||
for config_filename, config in configs.items():
|
for config_filename, config in configs.items():
|
||||||
try:
|
try:
|
||||||
json_results.extend(list(run_configuration(config_filename, config, args)))
|
json_results.extend(list(run_configuration(config_filename, config, arguments)))
|
||||||
yield logging.makeLogRecord(
|
yield logging.makeLogRecord(
|
||||||
dict(
|
dict(
|
||||||
levelno=logging.INFO,
|
levelno=logging.INFO,
|
||||||
@@ -470,7 +265,21 @@ def collect_configuration_run_summary_logs(configs, args):
|
|||||||
msg='{}: Successfully ran configuration file'.format(config_filename),
|
msg='{}: Successfully ran configuration file'.format(config_filename),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except (ValueError, OSError, CalledProcessError) as error:
|
except CalledProcessError as error:
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(
|
||||||
|
levelno=logging.CRITICAL,
|
||||||
|
levelname='CRITICAL',
|
||||||
|
msg='{}: Error running configuration file'.format(config_filename),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output)
|
||||||
|
)
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
||||||
|
)
|
||||||
|
except (ValueError, OSError) as error:
|
||||||
yield logging.makeLogRecord(
|
yield logging.makeLogRecord(
|
||||||
dict(
|
dict(
|
||||||
levelno=logging.CRITICAL,
|
levelno=logging.CRITICAL,
|
||||||
@@ -490,7 +299,9 @@ def collect_configuration_run_summary_logs(configs, args):
|
|||||||
dict(
|
dict(
|
||||||
levelno=logging.CRITICAL,
|
levelno=logging.CRITICAL,
|
||||||
levelname='CRITICAL',
|
levelname='CRITICAL',
|
||||||
msg='{}: No configuration files found'.format(' '.join(args.config_paths)),
|
msg='{}: No configuration files found'.format(
|
||||||
|
' '.join(arguments['global'].config_paths)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -508,7 +319,7 @@ def main(): # pragma: no cover
|
|||||||
configure_signals()
|
configure_signals()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = parse_arguments(*sys.argv[1:])
|
arguments = parse_arguments(*sys.argv[1:])
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
configure_logging(logging.CRITICAL)
|
configure_logging(logging.CRITICAL)
|
||||||
logger.critical(error)
|
logger.critical(error)
|
||||||
@@ -520,22 +331,24 @@ def main(): # pragma: no cover
|
|||||||
logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
|
logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
|
||||||
exit_with_help_link()
|
exit_with_help_link()
|
||||||
|
|
||||||
if args.version:
|
global_arguments = arguments['global']
|
||||||
|
if global_arguments.version:
|
||||||
print(pkg_resources.require('borgmatic')[0].version)
|
print(pkg_resources.require('borgmatic')[0].version)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
|
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
|
||||||
configs, parse_logs = load_configurations(config_filenames)
|
configs, parse_logs = load_configurations(config_filenames)
|
||||||
|
|
||||||
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color, configs))
|
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
|
||||||
configure_logging(
|
configure_logging(
|
||||||
verbosity_to_log_level(args.verbosity), verbosity_to_log_level(args.syslog_verbosity)
|
verbosity_to_log_level(global_arguments.verbosity),
|
||||||
|
verbosity_to_log_level(global_arguments.syslog_verbosity),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('Ensuring legacy configuration is upgraded')
|
logger.debug('Ensuring legacy configuration is upgraded')
|
||||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||||
|
|
||||||
summary_logs = list(collect_configuration_run_summary_logs(configs, args))
|
summary_logs = list(collect_configuration_run_summary_logs(configs, arguments))
|
||||||
|
|
||||||
logger.info('')
|
logger.info('')
|
||||||
logger.info('summary:')
|
logger.info('summary:')
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ map:
|
|||||||
type: bool
|
type: bool
|
||||||
desc: Only store/extract numeric user and group identifiers. Defaults to false.
|
desc: Only store/extract numeric user and group identifiers. Defaults to false.
|
||||||
example: true
|
example: true
|
||||||
|
atime:
|
||||||
|
type: bool
|
||||||
|
desc: Store atime into archive. Defaults to true.
|
||||||
|
example: false
|
||||||
|
ctime:
|
||||||
|
type: bool
|
||||||
|
desc: Store ctime into archive. Defaults to true.
|
||||||
|
example: false
|
||||||
|
birthtime:
|
||||||
|
type: bool
|
||||||
|
desc: Store birthtime (creation date) into archive. Defaults to true.
|
||||||
|
example: false
|
||||||
read_special:
|
read_special:
|
||||||
type: bool
|
type: bool
|
||||||
desc: |
|
desc: |
|
||||||
|
|||||||
+19
-5
@@ -4,7 +4,11 @@ import subprocess
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
||||||
|
|
||||||
|
|
||||||
def execute_and_log_output(full_command, output_log_level, shell):
|
def execute_and_log_output(full_command, output_log_level, shell):
|
||||||
|
last_lines = []
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
|
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
|
||||||
)
|
)
|
||||||
@@ -14,10 +18,13 @@ def execute_and_log_output(full_command, output_log_level, shell):
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if line.startswith('borg: error:'):
|
# Keep the last few lines of output in case the command errors, and we need the output for
|
||||||
logger.error(line)
|
# the exception below.
|
||||||
else:
|
last_lines.append(line)
|
||||||
logger.log(output_log_level, line)
|
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||||
|
last_lines.pop(0)
|
||||||
|
|
||||||
|
logger.log(output_log_level, line)
|
||||||
|
|
||||||
remaining_output = process.stdout.read().rstrip().decode()
|
remaining_output = process.stdout.read().rstrip().decode()
|
||||||
if remaining_output: # pragma: no cover
|
if remaining_output: # pragma: no cover
|
||||||
@@ -25,7 +32,14 @@ def execute_and_log_output(full_command, output_log_level, shell):
|
|||||||
|
|
||||||
exit_code = process.poll()
|
exit_code = process.poll()
|
||||||
if exit_code != 0:
|
if exit_code != 0:
|
||||||
raise subprocess.CalledProcessError(exit_code, full_command)
|
# If an error occurs, include its output in the raised exception so that we don't
|
||||||
|
# inadvertently hide error output.
|
||||||
|
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||||
|
last_lines.insert(0, '...')
|
||||||
|
|
||||||
|
raise subprocess.CalledProcessError(
|
||||||
|
exit_code, ' '.join(full_command), '\n'.join(last_lines)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
||||||
|
|||||||
+11
-3
@@ -21,6 +21,14 @@ def to_bool(arg):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_console():
|
||||||
|
'''
|
||||||
|
Return whether the current console is "interactive". Meaning: Capable of
|
||||||
|
user input and not just something like a cron job.
|
||||||
|
'''
|
||||||
|
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
|
||||||
|
|
||||||
|
|
||||||
def should_do_markup(no_color, configs):
|
def should_do_markup(no_color, configs):
|
||||||
'''
|
'''
|
||||||
Given the value of the command-line no-color argument, and a dict of configuration filename to
|
Given the value of the command-line no-color argument, and a dict of configuration filename to
|
||||||
@@ -37,7 +45,7 @@ def should_do_markup(no_color, configs):
|
|||||||
if py_colors is not None:
|
if py_colors is not None:
|
||||||
return to_bool(py_colors)
|
return to_bool(py_colors)
|
||||||
|
|
||||||
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
|
return interactive_console()
|
||||||
|
|
||||||
|
|
||||||
LOG_LEVEL_TO_COLOR = {
|
LOG_LEVEL_TO_COLOR = {
|
||||||
@@ -82,9 +90,9 @@ def configure_logging(console_log_level, syslog_log_level=None):
|
|||||||
elif os.path.exists('/var/run/syslog'):
|
elif os.path.exists('/var/run/syslog'):
|
||||||
syslog_path = '/var/run/syslog'
|
syslog_path = '/var/run/syslog'
|
||||||
|
|
||||||
if syslog_path:
|
if syslog_path and not interactive_console():
|
||||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||||
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s \ufeff%(message)s'))
|
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
|
||||||
syslog_handler.setLevel(syslog_log_level)
|
syslog_handler.setLevel(syslog_log_level)
|
||||||
handlers = (console_handler, syslog_handler)
|
handlers = (console_handler, syslog_handler)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+4
-1
@@ -2,7 +2,10 @@ FROM python:3.7.3-alpine3.9 as borgmatic
|
|||||||
|
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
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 list info; do \
|
||||||
|
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||||
|
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||||
|
|
||||||
FROM node:11.15.0-alpine as html
|
FROM node:11.15.0-alpine as html
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,18 @@ run borgmatic's pruning, creating, or checking actions separately. For
|
|||||||
instance, the the following optional flags are available:
|
instance, the the following optional flags are available:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --prune
|
borgmatic prune
|
||||||
borgmatic --create
|
borgmatic create
|
||||||
borgmatic --check
|
borgmatic check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
|
||||||
|
`--prune`, `--create`, or `--check`. Or upgrade borgmatic!)
|
||||||
|
|
||||||
You can run with only one of these flags provided, or you can mix and match
|
You can run with only one of these flags 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
|
||||||
making backups with `--create` on a frequent schedule, while only running
|
making backups with `create` on a frequent schedule, while only running
|
||||||
expensive consistency checks with `--check` on a much less frequent basis from
|
expensive consistency checks with `check` on a much less frequent basis from
|
||||||
a separate cron job.
|
a separate cron job.
|
||||||
|
|
||||||
### Consistency check configuration
|
### Consistency check configuration
|
||||||
|
|||||||
@@ -29,16 +29,20 @@ functionality:
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --list
|
borgmatic list
|
||||||
borgmatic --info
|
borgmatic info
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
|
||||||
|
`--info`. Or upgrade borgmatic!)
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
||||||
present. Where those logs show up depends on your particular system. If you're
|
present and borgmatic is running in a non-interactive console. Where those
|
||||||
using systemd, try running `journalctl -xe`. Otherwise, try viewing
|
logs show up depends on your particular system. If you're using systemd, try
|
||||||
`/var/log/syslog` or similiar.
|
running `journalctl -xe`. Otherwise, try viewing `/var/log/syslog` or
|
||||||
|
similiar.
|
||||||
|
|
||||||
You can customize the log level used for syslog logging with the
|
You can customize the log level used for syslog logging with the
|
||||||
`--syslog-verbosity` flag, and this is independent from the console logging
|
`--syslog-verbosity` flag, and this is independent from the console logging
|
||||||
@@ -71,8 +75,8 @@ already has this rate limit disabled.
|
|||||||
## Scripting borgmatic
|
## Scripting borgmatic
|
||||||
|
|
||||||
To consume the output of borgmatic in other software, you can include an
|
To consume the output of borgmatic in other software, you can include an
|
||||||
optional `--json` flag with `--create`, `--list`, or `--info` to get the
|
optional `--json` flag with `create`, `list`, or `info` to get the output
|
||||||
output formatted as JSON.
|
formatted as JSON.
|
||||||
|
|
||||||
Note that when you specify the `--json` flag, Borg's other non-JSON output is
|
Note that when you specify the `--json` flag, Borg's other non-JSON output is
|
||||||
suppressed so as not to interfere with the captured JSON. Also note that JSON
|
suppressed so as not to interfere with the captured JSON. Also note that JSON
|
||||||
|
|||||||
@@ -5,12 +5,15 @@ title: How to restore a backup
|
|||||||
|
|
||||||
When the worst happens—or you want to test your backups—the first step is
|
When the worst happens—or you want to test your backups—the first step is
|
||||||
to figure out which archive to restore. A good way to do that is to use the
|
to figure out which archive to restore. A good way to do that is to use the
|
||||||
`--list` action:
|
`list` action:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --list
|
borgmatic list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
|
||||||
|
borgmatic!)
|
||||||
|
|
||||||
That should yield output looking something like:
|
That should yield output looking something like:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -22,9 +25,12 @@ Assuming that you want to restore the archive with the most up-to-date files
|
|||||||
and therefore the latest timestamp, run a command like:
|
and therefore the latest timestamp, run a command like:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --extract --archive host-2019-01-02T04:06:07.080910
|
borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
|
||||||
|
borgmatic!)
|
||||||
|
|
||||||
The `--archive` value is the name of the archive to restore. This extracts the
|
The `--archive` value is the name of the archive to restore. This extracts the
|
||||||
entire contents of the archive to the current directory, so make sure you're
|
entire contents of the archive to the current directory, so make sure you're
|
||||||
in the right place before running the command.
|
in the right place before running the command.
|
||||||
@@ -33,13 +39,13 @@ in the right place before running the command.
|
|||||||
## Repository selection
|
## Repository selection
|
||||||
|
|
||||||
If you have a single repository in your borgmatic configuration file(s), no
|
If you have a single repository in your borgmatic configuration file(s), no
|
||||||
problem: the `--extract` action figures out which repository to use.
|
problem: the `extract` action figures out which repository to use.
|
||||||
|
|
||||||
But if you have multiple repositories configured, then you'll need to specify
|
But if you have multiple repositories configured, then you'll need to specify
|
||||||
the repository path containing the archive to extract. Here's an example:
|
the repository path containing the archive to extract. Here's an example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --extract --repository repo.borg --archive host-2019-...
|
borgmatic extract --repository repo.borg --archive host-2019-...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Restore particular files
|
## Restore particular files
|
||||||
@@ -49,7 +55,7 @@ everything from an archive. To do that, tack on one or more `--restore-path`
|
|||||||
values. For instance:
|
values. For instance:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --extract --archive host-2019-... --restore-path /path/1 /path/2
|
borgmatic extract --archive host-2019-... --restore-path /path/1 /path/2
|
||||||
```
|
```
|
||||||
|
|
||||||
Like a whole-archive restore, this also restores into the current directory.
|
Like a whole-archive restore, this also restores into the current directory.
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ hooks:
|
|||||||
|
|
||||||
## Hook output
|
## Hook output
|
||||||
|
|
||||||
Any output produced by your hooks shows up both at the console and in syslog.
|
Any output produced by your hooks shows up both at the console and in syslog
|
||||||
For more information, read about <a
|
(when run in a non-interactive console). For more information, read about <a
|
||||||
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md">inspecting
|
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md">inspecting
|
||||||
your backups</a>.
|
your backups</a>.
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,12 @@ this step if you already have a Borg repository.) To create a repository, run
|
|||||||
a command like the following:
|
a command like the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
borgmatic --init --encryption repokey
|
borgmatic init --encryption repokey
|
||||||
```
|
```
|
||||||
|
|
||||||
|
(No borgmatic `init` action? Try the old-style `--init` flag, or upgrade
|
||||||
|
borgmatic!)
|
||||||
|
|
||||||
This uses the borgmatic configuration file you created above to determine
|
This uses the borgmatic configuration file you created above to determine
|
||||||
which local or remote repository to create, and encrypts it with the
|
which local or remote repository to create, and encrypts it with the
|
||||||
encryption passphrase specified there if one is provided. Read about [Borg
|
encryption passphrase specified there if one is provided. Read about [Borg
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ title: borgmatic command-line reference
|
|||||||
---
|
---
|
||||||
## borgmatic options
|
## borgmatic options
|
||||||
|
|
||||||
Here are all of the available borgmatic command-line options:
|
Here are all of the available borgmatic command-line options. This includes the separate options for
|
||||||
|
each action sub-command:
|
||||||
|
|
||||||
```
|
```
|
||||||
{% include borgmatic/command-line.txt %}
|
{% include borgmatic/command-line.txt %}
|
||||||
|
|||||||
@@ -3,4 +3,8 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
docker build --tag borgmatic-docs --file docs/Dockerfile .
|
docker build --tag borgmatic-docs --file docs/Dockerfile .
|
||||||
|
echo
|
||||||
|
echo "You can view dev docs at http://localhost:8080"
|
||||||
|
echo "Note that links within these docs will go to the online docs, so you will need to fiddle with URLs manually to stay in the dev docs."
|
||||||
|
echo
|
||||||
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ mv temp.yaml.uncommented temp.yaml
|
|||||||
for sub_command in prune create check list info; do
|
for sub_command in prune create check list info; do
|
||||||
echo "********** borg $sub_command **********"
|
echo "********** borg $sub_command **********"
|
||||||
|
|
||||||
for line in $(borgmatic --config temp.yaml --$sub_command -v 2 2>&1 | grep "borg\w* $sub_command") ; do
|
for line in $(borgmatic --config temp.yaml $sub_command -v 2 2>&1 | grep "borg\w* $sub_command") ; do
|
||||||
echo "$line" | grep '^-' >> borgmatic_borg_flags
|
echo "$line" | grep '^-' >> borgmatic_borg_flags
|
||||||
done
|
done
|
||||||
sort borgmatic_borg_flags > borgmatic_borg_flags.sorted
|
sort borgmatic_borg_flags > borgmatic_borg_flags.sorted
|
||||||
@@ -48,6 +48,17 @@ for sub_command in prune create check list info; do
|
|||||||
| grep -v '^--stats$' \
|
| grep -v '^--stats$' \
|
||||||
| grep -v '^--verbose$' \
|
| grep -v '^--verbose$' \
|
||||||
| grep -v '^--warning$' \
|
| grep -v '^--warning$' \
|
||||||
|
| grep -v '^--exclude' \
|
||||||
|
| grep -v '^--exclude-from' \
|
||||||
|
| grep -v '^--first' \
|
||||||
|
| grep -v '^--format' \
|
||||||
|
| grep -v '^--glob-archives' \
|
||||||
|
| grep -v '^--last' \
|
||||||
|
| grep -v '^--list-format' \
|
||||||
|
| grep -v '^--patterns-from' \
|
||||||
|
| grep -v '^--prefix' \
|
||||||
|
| grep -v '^--short' \
|
||||||
|
| grep -v '^--sort-by' \
|
||||||
| grep -v '^-h$' \
|
| grep -v '^-h$' \
|
||||||
>> all_borg_flags
|
>> all_borg_flags
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.3.8'
|
VERSION = '1.3.12'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import pytest
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.commands import arguments as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||||
|
config_paths = ['default']
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||||
|
|
||||||
|
arguments = module.parse_arguments()
|
||||||
|
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
assert global_arguments.config_paths == config_paths
|
||||||
|
assert global_arguments.excludes_filename is None
|
||||||
|
assert global_arguments.verbosity == 0
|
||||||
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--config', 'myconfig', 'otherconfig')
|
||||||
|
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
||||||
|
assert global_arguments.verbosity == 0
|
||||||
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_verbosity_overrides_default():
|
||||||
|
config_paths = ['default']
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--verbosity', '1')
|
||||||
|
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
assert global_arguments.config_paths == config_paths
|
||||||
|
assert global_arguments.excludes_filename is None
|
||||||
|
assert global_arguments.verbosity == 1
|
||||||
|
assert global_arguments.syslog_verbosity == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||||
|
config_paths = ['default']
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--syslog-verbosity', '2')
|
||||||
|
|
||||||
|
global_arguments = arguments['global']
|
||||||
|
assert global_arguments.config_paths == config_paths
|
||||||
|
assert global_arguments.excludes_filename is None
|
||||||
|
assert global_arguments.verbosity == 0
|
||||||
|
assert global_arguments.syslog_verbosity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_list_json_overrides_default():
|
||||||
|
arguments = module.parse_arguments('list', '--json')
|
||||||
|
|
||||||
|
assert 'list' in arguments
|
||||||
|
assert arguments['list'].json is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_dashed_list_json_overrides_default():
|
||||||
|
arguments = module.parse_arguments('--list', '--json')
|
||||||
|
|
||||||
|
assert 'list' in arguments
|
||||||
|
assert arguments['list'].json is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments()
|
||||||
|
|
||||||
|
assert 'prune' in arguments
|
||||||
|
assert 'create' in arguments
|
||||||
|
assert 'check' in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys):
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exit:
|
||||||
|
module.parse_arguments('--help')
|
||||||
|
|
||||||
|
assert exit.value.code == 0
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'global arguments:' in captured.out
|
||||||
|
assert 'actions:' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_help_and_action_shows_action_help(capsys):
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exit:
|
||||||
|
module.parse_arguments('create', '--help')
|
||||||
|
|
||||||
|
assert exit.value.code == 0
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert 'global arguments:' not in captured.out
|
||||||
|
assert 'actions:' not in captured.out
|
||||||
|
assert 'create arguments:' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('prune')
|
||||||
|
|
||||||
|
assert 'prune' in arguments
|
||||||
|
assert 'create' not in arguments
|
||||||
|
assert 'check' not in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_dashed_prune_action_leaves_other_actions_disabled():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--prune')
|
||||||
|
|
||||||
|
assert 'prune' in arguments
|
||||||
|
assert 'create' not in arguments
|
||||||
|
assert 'check' not in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('create', 'check')
|
||||||
|
|
||||||
|
assert 'prune' not in arguments
|
||||||
|
assert 'create' in arguments
|
||||||
|
assert 'check' in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_multiple_dashed_actions_leaves_other_action_disabled():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--create', '--check')
|
||||||
|
|
||||||
|
assert 'prune' not in arguments
|
||||||
|
assert 'create' in arguments
|
||||||
|
assert 'check' in arguments
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_invalid_arguments_exits():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--posix-me-harder')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_deprecated_excludes_option():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_encryption_mode_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_encryption_mode_with_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_encryption_mode_with_dashed_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_requires_encryption_mode_with_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'init')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_append_only_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--append-only')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_storage_quota_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_init_and_prune():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'prune')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_init_and_create():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'init', '--encryption', 'repokey', 'create')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_init_and_dry_run():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments(
|
||||||
|
'--config', 'myconfig', 'init', '--encryption', 'repokey', '--dry-run'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--repository', 'test.borg')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_repository_with_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments(
|
||||||
|
'--config', 'myconfig', 'extract', '--repository', 'test.borg', '--archive', 'test'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_repository_with_list():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_archive_without_extract_or_list():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--archive', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_restore_paths_without_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--restore-path', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_archive_with_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'extract', '--archive', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_archive_with_dashed_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_archive_with_list():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'list', '--archive', 'test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_requires_archive_with_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--config', 'myconfig', 'extract')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_progress_before_create():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--progress', 'create', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_progress_after_create():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('create', '--progress', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_progress_and_extract():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--progress', 'extract', '--archive', 'test', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_progress_without_create():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--progress', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_stats_and_create_flags_does_not_raise():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--stats', 'create', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_stats_and_prune_flags_does_not_raise():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--stats', 'prune', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
module.parse_arguments('--stats', 'list')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_just_stats_flag_does_not_raise():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--stats')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_json_with_list_or_info():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('list', '--json')
|
||||||
|
module.parse_arguments('info', '--json')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_json_with_dashed_info():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
module.parse_arguments('--info', '--json')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_json_with_both_list_and_info():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('list', 'info', '--json')
|
||||||
@@ -1,281 +1,10 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import pytest
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.commands import borgmatic as module
|
from borgmatic.commands import borgmatic as module
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_no_arguments_uses_defaults():
|
|
||||||
config_paths = ['default']
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
|
||||||
|
|
||||||
parser = module.parse_arguments()
|
|
||||||
|
|
||||||
assert parser.config_paths == config_paths
|
|
||||||
assert parser.excludes_filename is None
|
|
||||||
assert parser.verbosity == 0
|
|
||||||
assert parser.syslog_verbosity == 0
|
|
||||||
assert parser.json is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
parser = module.parse_arguments('--config', 'myconfig', 'otherconfig')
|
|
||||||
|
|
||||||
assert parser.config_paths == ['myconfig', 'otherconfig']
|
|
||||||
assert parser.verbosity == 0
|
|
||||||
assert parser.syslog_verbosity == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_verbosity_overrides_default():
|
|
||||||
config_paths = ['default']
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
|
||||||
|
|
||||||
parser = module.parse_arguments('--verbosity', '1')
|
|
||||||
|
|
||||||
assert parser.config_paths == config_paths
|
|
||||||
assert parser.excludes_filename is None
|
|
||||||
assert parser.verbosity == 1
|
|
||||||
assert parser.syslog_verbosity == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
|
||||||
config_paths = ['default']
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
|
||||||
|
|
||||||
parser = module.parse_arguments('--syslog-verbosity', '2')
|
|
||||||
|
|
||||||
assert parser.config_paths == config_paths
|
|
||||||
assert parser.excludes_filename is None
|
|
||||||
assert parser.verbosity == 0
|
|
||||||
assert parser.syslog_verbosity == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_json_overrides_default():
|
|
||||||
parser = module.parse_arguments('--list', '--json')
|
|
||||||
assert parser.json is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
parser = module.parse_arguments()
|
|
||||||
|
|
||||||
assert parser.prune is True
|
|
||||||
assert parser.create is True
|
|
||||||
assert parser.check is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
parser = module.parse_arguments('--prune')
|
|
||||||
|
|
||||||
assert parser.prune is True
|
|
||||||
assert parser.create is False
|
|
||||||
assert parser.check is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
parser = module.parse_arguments('--create', '--check')
|
|
||||||
|
|
||||||
assert parser.prune is False
|
|
||||||
assert parser.create is True
|
|
||||||
assert parser.check is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_invalid_arguments_exits():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
|
||||||
module.parse_arguments('--posix-me-harder')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_deprecated_excludes_option():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_encryption_mode_without_init():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_encryption_mode_with_init():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_requires_encryption_mode_with_init():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--init')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_append_only_without_init():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--append-only')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_storage_quota_without_init():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_init_and_prune():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--prune')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_init_and_create():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--create')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_init_and_dry_run():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments(
|
|
||||||
'--config', 'myconfig', '--init', '--encryption', 'repokey', '--dry-run'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--repository', 'test.borg')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_repository_with_extract():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments(
|
|
||||||
'--config', 'myconfig', '--extract', '--repository', 'test.borg', '--archive', 'test'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_repository_with_list():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--list', '--repository', 'test.borg')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_archive_without_extract_or_list():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--archive', 'test')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_restore_paths_without_extract():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--restore-path', 'test')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_archive_with_extract():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_archive_with_list():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--list', '--archive', 'test')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_requires_archive_with_extract():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--extract')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_progress_and_create():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--progress', '--create', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_progress_and_extract():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--progress', '--extract', '--archive', 'test', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_progress_without_create():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--progress', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_stats_and_create_flags_does_not_raise():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--stats', '--create', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_stats_and_prune_flags_does_not_raise():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--stats', '--prune', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--stats', '--list')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_just_stats_flag_does_not_raise():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--stats')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_allows_json_with_list_or_info():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
module.parse_arguments('--list', '--json')
|
|
||||||
module.parse_arguments('--info', '--json')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_json_without_list_or_info():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--json')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_json_with_both_list_and_info():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--list', '--info', '--json')
|
|
||||||
|
|
||||||
|
|
||||||
def test_borgmatic_version_matches_news_version():
|
def test_borgmatic_version_matches_news_version():
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,22 @@ def test_execute_and_log_output_logs_each_line_separately():
|
|||||||
module.execute_and_log_output(['echo', 'there'], output_log_level=logging.INFO, shell=False)
|
module.execute_and_log_output(['echo', 'there'], output_log_level=logging.INFO, shell=False)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_and_log_output_logs_borg_error_as_error():
|
def test_execute_and_log_output_includes_borg_error_output_in_exception():
|
||||||
flexmock(module.logger).should_receive('error').with_args('borg: error: oopsie').once()
|
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||||
|
module.execute_and_log_output(['grep'], output_log_level=logging.INFO, shell=False)
|
||||||
|
|
||||||
module.execute_and_log_output(
|
assert error.value.returncode == 2
|
||||||
['echo', 'borg: error: oopsie'], output_log_level=logging.INFO, shell=False
|
assert error.value.output
|
||||||
)
|
|
||||||
|
|
||||||
|
def test_execute_and_log_output_truncates_long_borg_error_output():
|
||||||
|
flexmock(module).ERROR_OUTPUT_MAX_LINE_COUNT = 0
|
||||||
|
|
||||||
|
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||||
|
module.execute_and_log_output(['grep'], output_log_level=logging.INFO, shell=False)
|
||||||
|
|
||||||
|
assert error.value.returncode == 2
|
||||||
|
assert error.value.output.startswith('...')
|
||||||
|
|
||||||
|
|
||||||
def test_execute_and_log_output_with_no_output_logs_nothing():
|
def test_execute_and_log_output_with_no_output_logs_nothing():
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.borg import create as module
|
from borgmatic.borg import create as module
|
||||||
@@ -563,7 +564,8 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parameter():
|
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||||
|
def test_create_archive_with_option_true_calls_borg_without_corresponding_parameter(option_name):
|
||||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||||
@@ -579,21 +581,22 @@ def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parame
|
|||||||
location_config={
|
location_config={
|
||||||
'source_directories': ['foo', 'bar'],
|
'source_directories': ['foo', 'bar'],
|
||||||
'repositories': ['repo'],
|
'repositories': ['repo'],
|
||||||
'bsd_flags': True,
|
option_name: True,
|
||||||
'exclude_patterns': None,
|
'exclude_patterns': None,
|
||||||
},
|
},
|
||||||
storage_config={},
|
storage_config={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_parameter():
|
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||||
|
def test_create_archive_with_option_false_calls_borg_with_corresponding_parameter(option_name):
|
||||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
CREATE_COMMAND + ('--nobsdflags',), output_log_level=logging.INFO
|
CREATE_COMMAND + ('--no' + option_name.replace('_', ''),), output_log_level=logging.INFO
|
||||||
)
|
)
|
||||||
|
|
||||||
module.create_archive(
|
module.create_archive(
|
||||||
@@ -602,7 +605,7 @@ def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_paramete
|
|||||||
location_config={
|
location_config={
|
||||||
'source_directories': ['foo', 'bar'],
|
'source_directories': ['foo', 'bar'],
|
||||||
'repositories': ['repo'],
|
'repositories': ['repo'],
|
||||||
'bsd_flags': False,
|
option_name: False,
|
||||||
'exclude_patterns': None,
|
'exclude_patterns': None,
|
||||||
},
|
},
|
||||||
storage_config={},
|
storage_config={},
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.borg import flags as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_formats_string_value():
|
||||||
|
assert module.make_flags('foo', 'bar') == ('--foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_formats_integer_value():
|
||||||
|
assert module.make_flags('foo', 3) == ('--foo', '3')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_formats_true_value():
|
||||||
|
assert module.make_flags('foo', True) == ('--foo',)
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_omits_false_value():
|
||||||
|
assert module.make_flags('foo', False) == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_formats_name_with_underscore():
|
||||||
|
assert module.make_flags('posix_me_harder', 'okay') == ('--posix-me-harder', 'okay')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_from_arguments_flattens_and_sorts_multiple_arguments():
|
||||||
|
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||||
|
flexmock(module).should_receive('make_flags').with_args('baz', 'quux').and_return(
|
||||||
|
('baz', 'quux')
|
||||||
|
)
|
||||||
|
arguments = flexmock(foo='bar', baz='quux')
|
||||||
|
|
||||||
|
assert module.make_flags_from_arguments(arguments) == ('baz', 'quux', 'foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_from_arguments_excludes_underscored_argument_names():
|
||||||
|
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||||
|
arguments = flexmock(foo='bar', _baz='quux')
|
||||||
|
|
||||||
|
assert module.make_flags_from_arguments(arguments) == ('foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_flags_from_arguments_omits_excludes():
|
||||||
|
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||||
|
arguments = flexmock(foo='bar', baz='quux')
|
||||||
|
|
||||||
|
assert module.make_flags_from_arguments(arguments, excludes=('baz', 'other')) == ('foo', 'bar')
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.borg import info as module
|
from borgmatic.borg import info as module
|
||||||
@@ -14,7 +15,9 @@ def test_display_archives_info_calls_borg_with_parameters():
|
|||||||
INFO_COMMAND, output_log_level=logging.WARNING
|
INFO_COMMAND, output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.display_archives_info(repository='repo', storage_config={})
|
module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||||
@@ -22,7 +25,9 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
|||||||
INFO_COMMAND + ('--info',), output_log_level=logging.WARNING
|
INFO_COMMAND + ('--info',), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
module.display_archives_info(repository='repo', storage_config={})
|
module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
@@ -31,7 +36,9 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
|
|||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
json_output = module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
assert json_output == '[]'
|
assert json_output == '[]'
|
||||||
|
|
||||||
@@ -42,7 +49,9 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
|||||||
)
|
)
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
module.display_archives_info(repository='repo', storage_config={})
|
module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
@@ -51,7 +60,9 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
|
|||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
json_output = module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
assert json_output == '[]'
|
assert json_output == '[]'
|
||||||
|
|
||||||
@@ -61,17 +72,34 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
|||||||
INFO_COMMAND + ('--json',), output_log_level=None
|
INFO_COMMAND + ('--json',), output_log_level=None
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
json_output = module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
assert json_output == '[]'
|
assert json_output == '[]'
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING
|
||||||
|
)
|
||||||
|
|
||||||
|
module.display_archives_info(
|
||||||
|
repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||||
flexmock(module).should_receive('execute_command').with_args(
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
('borg1',) + INFO_COMMAND[1:], output_log_level=logging.WARNING
|
('borg1',) + INFO_COMMAND[1:], output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.display_archives_info(repository='repo', storage_config={}, local_path='borg1')
|
module.display_archives_info(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
info_arguments=flexmock(archive=None, json=False),
|
||||||
|
local_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
@@ -79,7 +107,12 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
|
|||||||
INFO_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
|
INFO_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.display_archives_info(repository='repo', storage_config={}, remote_path='borg1')
|
module.display_archives_info(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
info_arguments=flexmock(archive=None, json=False),
|
||||||
|
remote_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
@@ -88,4 +121,22 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
|
|||||||
INFO_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
|
INFO_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.display_archives_info(repository='repo', storage_config=storage_config)
|
module.display_archives_info(
|
||||||
|
repository='repo',
|
||||||
|
storage_config=storage_config,
|
||||||
|
info_arguments=flexmock(archive=None, json=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last'))
|
||||||
|
def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
INFO_COMMAND + ('--' + argument_name.replace('_', '-'), 'value'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
)
|
||||||
|
|
||||||
|
module.display_archives_info(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.borg import list as module
|
from borgmatic.borg import list as module
|
||||||
@@ -14,7 +15,9 @@ def test_list_archives_calls_borg_with_parameters():
|
|||||||
LIST_COMMAND, output_log_level=logging.WARNING
|
LIST_COMMAND, output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={})
|
module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
||||||
@@ -23,7 +26,9 @@ def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
|||||||
)
|
)
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={})
|
module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
||||||
@@ -32,7 +37,9 @@ def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
|||||||
)
|
)
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={}, json=True)
|
module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
@@ -41,7 +48,9 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
|||||||
)
|
)
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={})
|
module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
||||||
@@ -50,7 +59,9 @@ def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
|||||||
)
|
)
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={}, json=True)
|
module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
@@ -59,7 +70,11 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
|||||||
LIST_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
|
LIST_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config=storage_config)
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config=storage_config,
|
||||||
|
list_arguments=flexmock(archive=None, json=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
||||||
@@ -68,7 +83,11 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
|||||||
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING
|
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config=storage_config, archive='archive')
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config=storage_config,
|
||||||
|
list_arguments=flexmock(archive='archive', json=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_local_path_calls_borg_via_local_path():
|
def test_list_archives_with_local_path_calls_borg_via_local_path():
|
||||||
@@ -76,7 +95,12 @@ def test_list_archives_with_local_path_calls_borg_via_local_path():
|
|||||||
('borg1',) + LIST_COMMAND[1:], output_log_level=logging.WARNING
|
('borg1',) + LIST_COMMAND[1:], output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={}, local_path='borg1')
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
list_arguments=flexmock(archive=None, json=False),
|
||||||
|
local_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters():
|
def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
@@ -84,7 +108,51 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
|
|||||||
LIST_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
|
LIST_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(repository='repo', storage_config={}, remote_path='borg1')
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
list_arguments=flexmock(archive=None, json=False),
|
||||||
|
remote_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_archives_with_short_calls_borg_with_short_parameter():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
LIST_COMMAND + ('--short',), output_log_level=logging.WARNING
|
||||||
|
).and_return('[]')
|
||||||
|
|
||||||
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
list_arguments=flexmock(archive=None, json=False, short=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'argument_name',
|
||||||
|
(
|
||||||
|
'prefix',
|
||||||
|
'glob_archives',
|
||||||
|
'sort_by',
|
||||||
|
'first',
|
||||||
|
'last',
|
||||||
|
'exclude',
|
||||||
|
'exclude_from',
|
||||||
|
'pattern',
|
||||||
|
'pattern_from',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
LIST_COMMAND + ('--' + argument_name.replace('_', '-'), 'value'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
).and_return('[]')
|
||||||
|
|
||||||
|
module.list_archives(
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
list_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_list_archives_with_json_calls_borg_with_json_parameter():
|
def test_list_archives_with_json_calls_borg_with_json_parameter():
|
||||||
@@ -92,6 +160,8 @@ def test_list_archives_with_json_calls_borg_with_json_parameter():
|
|||||||
LIST_COMMAND + ('--json',), output_log_level=None
|
LIST_COMMAND + ('--json',), output_log_level=None
|
||||||
).and_return('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
json_output = module.list_archives(repository='repo', storage_config={}, json=True)
|
json_output = module.list_archives(
|
||||||
|
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||||
|
)
|
||||||
|
|
||||||
assert json_output == '[]'
|
assert json_output == '[]'
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.commands import arguments as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
|
||||||
|
global_namespace = flexmock()
|
||||||
|
action_namespace = flexmock(foo=True)
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('--foo', 'true', 'action'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
|
||||||
|
global_namespace = flexmock()
|
||||||
|
action_namespace = flexmock(foo=True)
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('action', '--foo', 'true'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_global_arguments_before_subparser_name():
|
||||||
|
global_namespace = flexmock(verbosity='lots')
|
||||||
|
action_namespace = flexmock()
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(
|
||||||
|
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||||
|
),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_global_arguments_after_subparser_name():
|
||||||
|
global_namespace = flexmock(verbosity='lots')
|
||||||
|
action_namespace = flexmock()
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(
|
||||||
|
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||||
|
),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('action', '--verbosity', 'lots'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
||||||
|
global_namespace = flexmock()
|
||||||
|
action_namespace = flexmock(foo=True)
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': action_subparser,
|
||||||
|
'-a': action_subparser,
|
||||||
|
'other': flexmock(),
|
||||||
|
'-o': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('-a', '--foo', 'true'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
||||||
|
global_namespace = flexmock()
|
||||||
|
action_namespace = flexmock(foo=True)
|
||||||
|
other_namespace = flexmock(bar=3)
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(
|
||||||
|
parse_known_args=lambda arguments: (action_namespace, ['--bar', '3'])
|
||||||
|
),
|
||||||
|
'other': flexmock(parse_known_args=lambda arguments: (other_namespace, [])),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('action', '--foo', 'true', 'other', '--bar', '3'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {
|
||||||
|
'action': action_namespace,
|
||||||
|
'other': other_namespace,
|
||||||
|
'global': global_namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_applies_default_subparsers():
|
||||||
|
global_namespace = flexmock()
|
||||||
|
prune_namespace = flexmock()
|
||||||
|
create_namespace = flexmock(progress=True)
|
||||||
|
check_namespace = flexmock()
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
|
||||||
|
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
|
||||||
|
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(('--progress'), top_level_parser, subparsers)
|
||||||
|
|
||||||
|
assert arguments == {
|
||||||
|
'prune': prune_namespace,
|
||||||
|
'create': create_namespace,
|
||||||
|
'check': check_namespace,
|
||||||
|
'global': global_namespace,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers():
|
||||||
|
global_namespace = flexmock(verbosity='lots')
|
||||||
|
action_namespace = flexmock()
|
||||||
|
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||||
|
subparsers = flexmock(
|
||||||
|
choices={
|
||||||
|
'action': flexmock(
|
||||||
|
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||||
|
),
|
||||||
|
'other': flexmock(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = module.parse_subparser_arguments(
|
||||||
|
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
|
||||||
|
)
|
||||||
|
|
||||||
|
assert arguments == {'global': global_namespace}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic.commands import borgmatic as module
|
from borgmatic.commands import borgmatic as module
|
||||||
@@ -27,9 +29,11 @@ def test_load_configurations_logs_critical_for_parse_error():
|
|||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
args = flexmock(extract=False, list=False)
|
arguments = {}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||||
|
|
||||||
@@ -37,9 +41,11 @@ def test_collect_configuration_run_summary_logs_info_for_success():
|
|||||||
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
args = flexmock(extract=True, list=False, repository='repo')
|
arguments = {'extract': flexmock(repository='repo')}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||||
|
|
||||||
@@ -48,9 +54,11 @@ def test_collect_configuration_run_summary_logs_critical_for_extract_with_reposi
|
|||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||||
ValueError
|
ValueError
|
||||||
)
|
)
|
||||||
args = flexmock(extract=True, list=False, repository='repo')
|
arguments = {'extract': flexmock(repository='repo')}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||||
|
|
||||||
@@ -59,50 +67,71 @@ def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_a
|
|||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||||
ValueError
|
ValueError
|
||||||
)
|
)
|
||||||
args = flexmock(extract=False, list=True, repository='repo', archive='test')
|
arguments = {'list': flexmock(repository='repo', archive='test')}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
args = flexmock(extract=False, list=True, repository='repo', archive=None)
|
arguments = {'list': flexmock(repository='repo', archive=None)}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_run_error():
|
def test_collect_configuration_run_summary_logs_critical_for_run_value_error():
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||||
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
|
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
|
||||||
args = flexmock(extract=False, list=False)
|
arguments = {}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_configuration_run_summary_logs_critical_including_output_for_run_process_error():
|
||||||
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||||
|
flexmock(module).should_receive('run_configuration').and_raise(
|
||||||
|
subprocess.CalledProcessError(1, 'command', 'error output')
|
||||||
|
)
|
||||||
|
arguments = {}
|
||||||
|
|
||||||
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||||
|
assert any(log for log in logs if 'error output' in str(log))
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||||
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
|
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
|
||||||
['baz']
|
['baz']
|
||||||
)
|
)
|
||||||
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
|
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
|
||||||
args = flexmock(extract=False, list=False)
|
arguments = {}
|
||||||
|
|
||||||
tuple(
|
tuple(
|
||||||
module.collect_configuration_run_summary_logs(
|
module.collect_configuration_run_summary_logs(
|
||||||
{'test.yaml': {}, 'test2.yaml': {}}, args=args
|
{'test.yaml': {}, 'test2.yaml': {}}, arguments=arguments
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
|
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
args = flexmock(extract=False, list=False, config_paths=[])
|
arguments = {'global': flexmock(config_paths=[])}
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({}, args=args))
|
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||||
|
|||||||
@@ -20,6 +20,29 @@ def test_to_bool_passes_none_through():
|
|||||||
assert module.to_bool(None) is None
|
assert module.to_bool(None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_interactive_console_false_when_not_isatty(capsys):
|
||||||
|
with capsys.disabled():
|
||||||
|
flexmock(module.sys.stdout).should_receive('isatty').and_return(False)
|
||||||
|
|
||||||
|
assert module.interactive_console() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_interactive_console_false_when_TERM_is_dumb(capsys):
|
||||||
|
with capsys.disabled():
|
||||||
|
flexmock(module.sys.stdout).should_receive('isatty').and_return(True)
|
||||||
|
flexmock(module.os.environ).should_receive('get').with_args('TERM').and_return('dumb')
|
||||||
|
|
||||||
|
assert module.interactive_console() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_interactive_console_true_when_isatty_and_TERM_is_not_dumb(capsys):
|
||||||
|
with capsys.disabled():
|
||||||
|
flexmock(module.sys.stdout).should_receive('isatty').and_return(True)
|
||||||
|
flexmock(module.os.environ).should_receive('get').with_args('TERM').and_return('smart')
|
||||||
|
|
||||||
|
assert module.interactive_console() is True
|
||||||
|
|
||||||
|
|
||||||
def test_should_do_markup_respects_no_color_value():
|
def test_should_do_markup_respects_no_color_value():
|
||||||
assert module.should_do_markup(no_color=True, configs={}) is False
|
assert module.should_do_markup(no_color=True, configs={}) is False
|
||||||
|
|
||||||
@@ -75,15 +98,17 @@ def test_should_do_markup_prefers_no_color_value_to_PY_COLORS():
|
|||||||
assert module.should_do_markup(no_color=True, configs={}) is False
|
assert module.should_do_markup(no_color=True, configs={}) is False
|
||||||
|
|
||||||
|
|
||||||
def test_should_do_markup_respects_stdout_tty_value():
|
def test_should_do_markup_respects_interactive_console_value():
|
||||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(True)
|
||||||
|
|
||||||
assert module.should_do_markup(no_color=False, configs={}) is False
|
assert module.should_do_markup(no_color=False, configs={}) is True
|
||||||
|
|
||||||
|
|
||||||
def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value():
|
def test_should_do_markup_prefers_PY_COLORS_to_interactive_console_value():
|
||||||
flexmock(module.os.environ).should_receive('get').and_return('True')
|
flexmock(module.os.environ).should_receive('get').and_return('True')
|
||||||
flexmock(module).should_receive('to_bool').and_return(True)
|
flexmock(module).should_receive('to_bool').and_return(True)
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
|
|
||||||
assert module.should_do_markup(no_color=False, configs={}) is True
|
assert module.should_do_markup(no_color=False, configs={}) is True
|
||||||
|
|
||||||
@@ -108,11 +133,11 @@ def test_color_text_without_color_does_not_raise():
|
|||||||
|
|
||||||
def test_configure_logging_probes_for_log_socket_on_linux():
|
def test_configure_logging_probes_for_log_socket_on_linux():
|
||||||
flexmock(module).should_receive('Console_color_formatter')
|
flexmock(module).should_receive('Console_color_formatter')
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
level=logging.INFO, handlers=tuple
|
level=logging.INFO, handlers=tuple
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||||
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(False)
|
|
||||||
syslog_handler = logging.handlers.SysLogHandler()
|
syslog_handler = logging.handlers.SysLogHandler()
|
||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
|
||||||
address='/dev/log'
|
address='/dev/log'
|
||||||
@@ -123,6 +148,7 @@ def test_configure_logging_probes_for_log_socket_on_linux():
|
|||||||
|
|
||||||
def test_configure_logging_probes_for_log_socket_on_macos():
|
def test_configure_logging_probes_for_log_socket_on_macos():
|
||||||
flexmock(module).should_receive('Console_color_formatter')
|
flexmock(module).should_receive('Console_color_formatter')
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
level=logging.INFO, handlers=tuple
|
level=logging.INFO, handlers=tuple
|
||||||
)
|
)
|
||||||
@@ -155,3 +181,15 @@ def test_configure_logging_skips_syslog_if_not_found():
|
|||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||||
|
|
||||||
module.configure_logging(console_log_level=logging.INFO)
|
module.configure_logging(console_log_level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_configure_logging_skips_syslog_if_interactive_console():
|
||||||
|
flexmock(module).should_receive('Console_color_formatter')
|
||||||
|
flexmock(module).should_receive('interactive_console').and_return(True)
|
||||||
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
|
level=logging.INFO, handlers=tuple
|
||||||
|
)
|
||||||
|
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||||
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||||
|
|
||||||
|
module.configure_logging(console_log_level=logging.INFO)
|
||||||
|
|||||||
Reference in New Issue
Block a user