mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a897ffd514 | ||
|
|
a472735616 | ||
|
|
b3fec03cf4 | ||
|
|
89dccc25c3 | ||
|
|
3846155d62 | ||
|
|
386979ebb4 | ||
|
|
07222cd984 | ||
|
|
cf4c6c274d | ||
|
|
340bd72176 | ||
|
|
1a1bb71af1 | ||
|
|
ae45dfe63a | ||
|
|
d6ac7a9192 | ||
|
|
d959fdbf8d |
+6
-6
@@ -1,30 +1,30 @@
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-5-alpine-3-9
|
||||
name: python-3-5-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.5-alpine3.9
|
||||
image: python:3.5-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-6-alpine-3-9
|
||||
name: python-3-6-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.6-alpine3.9
|
||||
image: python:3.6-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-7-alpine-3-9
|
||||
name: python-3-7-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.7-alpine3.9
|
||||
image: python:3.7-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
include borgmatic/config/schema.yaml
|
||||
graft sample/systemd
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
1.3.20
|
||||
* #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO
|
||||
priority, etc.
|
||||
* #221: Fix "borgmatic create --progress" output so that it updates on the console in real-time.
|
||||
|
||||
1.3.19
|
||||
* #219: Fix visibility of "borgmatic prune --stats" output.
|
||||
|
||||
1.3.18
|
||||
* #220: Fix regression of argument parsing for default actions.
|
||||
|
||||
1.3.17
|
||||
* #217: Fix error with "borgmatic check --only" command-line flag with "extract" consistency check.
|
||||
|
||||
1.3.16
|
||||
* #210: Support for Borg check --verify-data flag via borgmatic "data" consistency check.
|
||||
* #210: Override configured consistency checks via "borgmatic check --only" command-line flag.
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -163,6 +163,12 @@ def create_archive(
|
||||
+ sources
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
return
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif stats:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -83,4 +83,10 @@ def extract_archive(
|
||||
+ (tuple(restore_paths) if restore_paths else ())
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
return
|
||||
|
||||
execute_command(full_command)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from borgmatic.execute import BORG_ERROR_EXIT_CODE, execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,8 +45,4 @@ def initialize_repository(
|
||||
)
|
||||
|
||||
# Don't use execute_command() here because it doesn't support interactive prompts.
|
||||
try:
|
||||
subprocess.check_call(init_command)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
||||
raise
|
||||
execute_command_without_capture(init_command)
|
||||
|
||||
@@ -64,4 +64,4 @@ def prune_archives(
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command)
|
||||
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
|
||||
|
||||
@@ -14,14 +14,14 @@ SUBPARSER_ALIASES = {
|
||||
}
|
||||
|
||||
|
||||
def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
|
||||
def parse_subparser_arguments(unparsed_arguments, 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.
|
||||
Given a sequence of arguments, and a subparsers object as returned by
|
||||
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
|
||||
parsing all arguments. This allows common arguments like "--repository" to be shared across
|
||||
multiple subparsers.
|
||||
|
||||
Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
|
||||
arguments.
|
||||
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
|
||||
'''
|
||||
arguments = collections.OrderedDict()
|
||||
remaining_arguments = list(unparsed_arguments)
|
||||
@@ -31,35 +31,73 @@ def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
|
||||
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:
|
||||
if subparser_name not in remaining_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)
|
||||
# If a parsed value happens to be the same as the name of a subparser, remove it from the
|
||||
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
|
||||
# the "extract" subparser.
|
||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||
for value in vars(parsed).values():
|
||||
if isinstance(value, str):
|
||||
if value in subparsers.choices:
|
||||
remaining_arguments.remove(value)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
if item in subparsers.choices:
|
||||
remaining_arguments.remove(item)
|
||||
|
||||
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)
|
||||
parsed, unused_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_global_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(), parse and return any global
|
||||
arguments as a parsed argparse.Namespace instance.
|
||||
'''
|
||||
# Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
|
||||
# are global arguments.
|
||||
remaining_arguments = list(unparsed_arguments)
|
||||
present_subparser_names = set()
|
||||
|
||||
for subparser_name, subparser in subparsers.choices.items():
|
||||
if subparser_name not in remaining_arguments:
|
||||
continue
|
||||
|
||||
present_subparser_names.add(subparser_name)
|
||||
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||
|
||||
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
||||
if (
|
||||
not present_subparser_names
|
||||
and '--help' not in unparsed_arguments
|
||||
and '-h' not in unparsed_arguments
|
||||
):
|
||||
for subparser_name in ('prune', 'create', 'check'):
|
||||
subparser = subparsers.choices[subparser_name]
|
||||
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||
|
||||
# Remove the subparser names themselves.
|
||||
for subparser_name in present_subparser_names:
|
||||
if subparser_name in remaining_arguments:
|
||||
remaining_arguments.remove(subparser_name)
|
||||
|
||||
return top_level_parser.parse_args(remaining_arguments)
|
||||
|
||||
|
||||
def parse_arguments(*unparsed_arguments):
|
||||
'''
|
||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||
@@ -218,7 +256,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
choices=('repository', 'archives', 'data', 'extract'),
|
||||
dest='only',
|
||||
action='append',
|
||||
help='Run a particular consistency check instead of configured checks (can specify multiple times)',
|
||||
help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
|
||||
)
|
||||
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
@@ -339,7 +377,8 @@ def parse_arguments(*unparsed_arguments):
|
||||
)
|
||||
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)
|
||||
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
|
||||
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
|
||||
|
||||
if arguments['global'].excludes_filename:
|
||||
raise ValueError(
|
||||
|
||||
@@ -61,3 +61,18 @@ def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
||||
return output.decode() if output is not None else None
|
||||
else:
|
||||
execute_and_log_output(full_command, output_log_level, shell=shell)
|
||||
|
||||
|
||||
def execute_command_without_capture(full_command):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings), but don't capture or log its
|
||||
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
|
||||
display) or provide interactive prompts.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
|
||||
try:
|
||||
subprocess.check_call(full_command)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
||||
raise
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM python:3.7.3-alpine3.10 as borgmatic
|
||||
FROM python:3.7.4-alpine3.10 as borgmatic
|
||||
|
||||
COPY . /app
|
||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
@@ -7,7 +7,7 @@ RUN borgmatic --help > /command-line.txt \
|
||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||
|
||||
FROM node:12.4.0-alpine as html
|
||||
FROM node:12.10.0-alpine as html
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
@@ -23,7 +23,7 @@ COPY . /source
|
||||
RUN npx eleventy --input=/source/docs --output=/output/docs \
|
||||
&& mv /output/docs/index.html /output/index.html
|
||||
|
||||
FROM nginx:1.16.0-alpine
|
||||
FROM nginx:1.16.1-alpine
|
||||
|
||||
COPY --from=html /output /usr/share/nginx/html
|
||||
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
|
||||
|
||||
@@ -68,6 +68,16 @@ consistency:
|
||||
- path/of/repository_to_check.borg
|
||||
```
|
||||
|
||||
Finally, you can override your configuration file's consistency checks, and
|
||||
run particular checks via the command-line. For instance:
|
||||
|
||||
```bash
|
||||
borgmatic check --only data --only extract
|
||||
```
|
||||
|
||||
This is useful for running slow consistency checks on an infrequent basis,
|
||||
separate from your regular checks.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ Or, for even more progress and debug spew:
|
||||
borgmatic --verbosity 2
|
||||
```
|
||||
|
||||
## Backup summary
|
||||
|
||||
If you're less concerned with progress during a backup, and you just want to
|
||||
see the summary of archive statistics at the end, you can use the stats
|
||||
option:
|
||||
|
||||
```bash
|
||||
borgmatic --stats
|
||||
```
|
||||
|
||||
## Existing backups
|
||||
|
||||
Borgmatic provides convenient flags for Borg's
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
||||
|
||||
0 3 * * * PATH=$PATH:/usr/bin /root/.local/bin/borgmatic
|
||||
@@ -1,3 +1,3 @@
|
||||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
||||
|
||||
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
|
||||
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
[Unit]
|
||||
Description=borgmatic backup
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
ConditionACPower=true
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/.local/bin/borgmatic
|
||||
|
||||
# Lower CPU and I/O priority.
|
||||
Nice=19
|
||||
CPUSchedulingPolicy=batch
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
IOWeight=100
|
||||
|
||||
Restart=no
|
||||
LogRateLimitIntervalSec=0
|
||||
|
||||
# Delay start to prevent backups running during boot.
|
||||
ExecStartPre=sleep 1m
|
||||
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.3.16'
|
||||
VERSION = '1.3.20'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -4,21 +4,21 @@ attrs==19.1.0
|
||||
black==19.3b0; python_version >= '3.6'
|
||||
click==7.0
|
||||
colorama==0.4.1
|
||||
coverage==4.5.3
|
||||
coverage==4.5.4
|
||||
docopt==0.6.2
|
||||
flake8==3.7.7
|
||||
flake8==3.7.8
|
||||
flexmock==0.10.4
|
||||
isort==4.3.20
|
||||
isort==4.3.21
|
||||
mccabe==0.6.1
|
||||
more-itertools==7.0.0
|
||||
pluggy==0.12.0
|
||||
more-itertools==7.2.0
|
||||
pluggy==0.13.0
|
||||
py==1.8.0
|
||||
pycodestyle==2.5.0
|
||||
pyflakes==2.1.1
|
||||
pykwalify==1.7.0
|
||||
pytest==4.6.3
|
||||
pytest==5.1.2
|
||||
pytest-cov==2.7.1
|
||||
python-dateutil==2.8.0
|
||||
PyYAML==5.1.1
|
||||
PyYAML==5.1.2
|
||||
ruamel.yaml>0.15.0,<0.17.0
|
||||
toml==0.10.0
|
||||
|
||||
@@ -78,6 +78,18 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
||||
assert 'check' in arguments
|
||||
|
||||
|
||||
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
arguments = module.parse_arguments('--stats')
|
||||
|
||||
assert 'prune' in arguments
|
||||
assert arguments['prune'].stats
|
||||
assert 'create' in arguments
|
||||
assert arguments['create'].stats
|
||||
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'])
|
||||
|
||||
@@ -322,12 +334,6 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
|
||||
module.parse_arguments('--stats', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_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'])
|
||||
|
||||
@@ -346,3 +352,21 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments('list', 'info', '--json')
|
||||
|
||||
|
||||
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('check', '--only', 'extract')
|
||||
|
||||
|
||||
def test_parse_arguments_extract_archive_check_does_not_raise_check_subparser_error():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('extract', '--archive', 'check')
|
||||
|
||||
|
||||
def test_parse_arguments_extract_with_check_only_extract_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('extract', '--archive', 'name', 'check', '--only', 'extract')
|
||||
|
||||
@@ -758,6 +758,29 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
progress=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
|
||||
@@ -195,7 +195,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_progress_parameter():
|
||||
insert_execute_command_mock(('borg', 'extract', '--progress', 'repo::archive'))
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'extract', '--progress', 'repo::archive')
|
||||
).once()
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
||||
@@ -23,8 +23,8 @@ def insert_info_command_not_found_mock():
|
||||
|
||||
|
||||
def insert_init_command_mock(init_command, **kwargs):
|
||||
flexmock(module.subprocess).should_receive('check_call').with_args(
|
||||
init_command, **kwargs
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
init_command
|
||||
).once()
|
||||
|
||||
|
||||
@@ -35,18 +35,9 @@ def test_initialize_repository_calls_borg_with_parameters():
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_does_not_raise_for_borg_init_warning():
|
||||
insert_info_command_not_found_mock()
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'borg init')
|
||||
)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_raises_for_borg_init_error():
|
||||
insert_info_command_not_found_mock()
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
flexmock(module).should_receive('execute_command_without_capture').and_raise(
|
||||
module.subprocess.CalledProcessError(2, 'borg init')
|
||||
)
|
||||
|
||||
@@ -56,7 +47,7 @@ def test_initialize_repository_raises_for_borg_init_error():
|
||||
|
||||
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
||||
insert_info_command_found_mock()
|
||||
flexmock(module.subprocess).should_receive('check_call').never()
|
||||
flexmock(module).should_receive('execute_command_without_capture').never()
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ from borgmatic.borg import prune as module
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(prune_command, **kwargs):
|
||||
flexmock(module).should_receive('execute_command').with_args(prune_command).once()
|
||||
def insert_execute_command_mock(prune_command, output_log_level):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
prune_command, output_log_level=output_log_level
|
||||
).once()
|
||||
|
||||
|
||||
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
|
||||
@@ -61,7 +63,7 @@ def test_prune_archives_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
|
||||
@@ -73,7 +75,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
@@ -87,7 +89,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo')
|
||||
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -101,7 +103,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
|
||||
@@ -113,7 +115,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',))
|
||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
@@ -129,7 +131,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
@@ -140,13 +142,29 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
retention_config=retention_config,
|
||||
stats=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
|
||||
storage_config = {'umask': '077'}
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
@@ -162,7 +180,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
|
||||
@@ -4,9 +4,7 @@ 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, [])),
|
||||
@@ -14,17 +12,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('--foo', 'true', 'action'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
assert arguments == {'action': action_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, [])),
|
||||
@@ -32,57 +26,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('action', '--foo', 'true'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), 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}
|
||||
assert arguments == {'action': action_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={
|
||||
@@ -94,18 +44,14 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
||||
)
|
||||
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('-a', '--foo', 'true'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
assert arguments == {'action': action_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(
|
||||
@@ -116,22 +62,16 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('action', '--foo', 'true', 'other', '--bar', '3'), top_level_parser, subparsers
|
||||
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
|
||||
)
|
||||
|
||||
assert arguments == {
|
||||
'action': action_namespace,
|
||||
'other': other_namespace,
|
||||
'global': global_namespace,
|
||||
}
|
||||
assert arguments == {'action': action_namespace, 'other': other_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'])),
|
||||
@@ -141,17 +81,16 @@ def test_parse_subparser_arguments_applies_default_subparsers():
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(('--progress'), top_level_parser, subparsers)
|
||||
arguments = module.parse_subparser_arguments(('--progress'), 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():
|
||||
def test_parse_global_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)
|
||||
@@ -164,8 +103,48 @@ def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers()
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
arguments = module.parse_global_arguments(
|
||||
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == {'global': global_namespace}
|
||||
assert arguments == global_namespace
|
||||
|
||||
|
||||
def test_parse_global_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_global_arguments(
|
||||
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == global_namespace
|
||||
|
||||
|
||||
def test_parse_global_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_global_arguments(
|
||||
('action', '--verbosity', 'lots'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == global_namespace
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic import execute as module
|
||||
@@ -49,3 +50,28 @@ def test_execute_command_captures_output_with_shell():
|
||||
output = module.execute_command(full_command, output_log_level=None, shell=True)
|
||||
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_success():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(0, 'borg init')
|
||||
)
|
||||
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_warning():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'borg init')
|
||||
)
|
||||
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
||||
|
||||
def test_execute_command_without_capture_raises_on_error():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(2, 'borg init')
|
||||
)
|
||||
|
||||
with pytest.raises(module.subprocess.CalledProcessError):
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
||||
Reference in New Issue
Block a user