Run arbitrary Borg commands with new "borgmatic borg" action (#425).
This commit is contained in:
parent
b37dd1a79e
commit
cf8882f2bc
13 changed files with 438 additions and 132 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,7 +2,7 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.swp
|
*.swp
|
||||||
.cache
|
.cache
|
||||||
.coverage
|
.coverage*
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.tox
|
.tox
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
2
NEWS
2
NEWS
|
@ -1,6 +1,8 @@
|
||||||
1.5.15.dev0
|
1.5.15.dev0
|
||||||
* #419: Document use case of running backups conditionally based on laptop power level:
|
* #419: Document use case of running backups conditionally based on laptop power level:
|
||||||
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
|
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
|
||||||
|
* #425: Run arbitrary Borg commands with new "borgmatic borg" action. See the documentation for
|
||||||
|
more information: https://torsion.org/borgmatic/docs/how-to/run-arbitrary-borg-commands/
|
||||||
|
|
||||||
1.5.14
|
1.5.14
|
||||||
* #390: Add link to Hetzner storage offering from the documentation.
|
* #390: Add link to Hetzner storage offering from the documentation.
|
||||||
|
|
45
borgmatic/borg/borg.py
Normal file
45
borgmatic/borg/borg.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from borgmatic.borg.flags import make_flags
|
||||||
|
from borgmatic.execute import execute_command
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
REPOSITORYLESS_BORG_COMMANDS = {'serve', None}
|
||||||
|
|
||||||
|
|
||||||
|
def run_arbitrary_borg(
|
||||||
|
repository, storage_config, options, archive=None, local_path='borg', remote_path=None
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Given a local or remote repository path, a storage config dict, a sequence of arbitrary
|
||||||
|
command-line Borg options, and an optional archive name, run an arbitrary Borg command on the
|
||||||
|
given repository/archive.
|
||||||
|
'''
|
||||||
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
options = options[1:] if options[0] == '--' else options
|
||||||
|
borg_command = options[0]
|
||||||
|
command_options = tuple(options[1:])
|
||||||
|
except IndexError:
|
||||||
|
borg_command = None
|
||||||
|
command_options = ()
|
||||||
|
|
||||||
|
repository_archive = '::'.join((repository, archive)) if repository and archive else repository
|
||||||
|
|
||||||
|
full_command = (
|
||||||
|
(local_path,)
|
||||||
|
+ ((borg_command,) if borg_command else ())
|
||||||
|
+ ((repository_archive,) if borg_command and repository_archive else ())
|
||||||
|
+ command_options
|
||||||
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
|
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
|
+ make_flags('remote-path', remote_path)
|
||||||
|
+ make_flags('lock-wait', lock_wait)
|
||||||
|
)
|
||||||
|
|
||||||
|
return execute_command(
|
||||||
|
full_command, output_log_level=logging.WARNING, borg_local_path=local_path,
|
||||||
|
)
|
|
@ -220,7 +220,8 @@ def create_archive(
|
||||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
|
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'create')
|
tuple(local_path.split(' '))
|
||||||
|
+ ('create',)
|
||||||
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
||||||
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
||||||
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
||||||
|
|
|
@ -15,17 +15,18 @@ SUBPARSER_ALIASES = {
|
||||||
'restore': ['--restore', '-r'],
|
'restore': ['--restore', '-r'],
|
||||||
'list': ['--list', '-l'],
|
'list': ['--list', '-l'],
|
||||||
'info': ['--info', '-i'],
|
'info': ['--info', '-i'],
|
||||||
|
'borg': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_subparser_arguments(unparsed_arguments, subparsers):
|
def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
'''
|
'''
|
||||||
Given a sequence of arguments, and a subparsers object as returned by
|
Given a sequence of arguments and a dict from subparser name to argparse.ArgumentParser
|
||||||
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
|
instance, give each requested action's subparser a shot at parsing all arguments. This allows
|
||||||
parsing all arguments. This allows common arguments like "--repository" to be shared across
|
common arguments like "--repository" to be shared across multiple subparsers.
|
||||||
multiple subparsers.
|
|
||||||
|
|
||||||
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
|
Return the result as a tuple of (a dict mapping from subparser name to a parsed namespace of
|
||||||
|
arguments, a list of remaining arguments not claimed by any subparser).
|
||||||
'''
|
'''
|
||||||
arguments = collections.OrderedDict()
|
arguments = collections.OrderedDict()
|
||||||
remaining_arguments = list(unparsed_arguments)
|
remaining_arguments = list(unparsed_arguments)
|
||||||
|
@ -35,7 +36,12 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
for alias in aliases
|
for alias in aliases
|
||||||
}
|
}
|
||||||
|
|
||||||
for subparser_name, subparser in subparsers.choices.items():
|
# If the "borg" action is used, skip all other subparsers. This avoids confusion like
|
||||||
|
# "borg list" triggering borgmatic's own list action.
|
||||||
|
if 'borg' in unparsed_arguments:
|
||||||
|
subparsers = {'borg': subparsers['borg']}
|
||||||
|
|
||||||
|
for subparser_name, subparser in subparsers.items():
|
||||||
if subparser_name not in remaining_arguments:
|
if subparser_name not in remaining_arguments:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -47,11 +53,11 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||||
for value in vars(parsed).values():
|
for value in vars(parsed).values():
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value in subparsers.choices:
|
if value in subparsers:
|
||||||
remaining_arguments.remove(value)
|
remaining_arguments.remove(value)
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
for item in value:
|
for item in value:
|
||||||
if item in subparsers.choices:
|
if item in subparsers:
|
||||||
remaining_arguments.remove(item)
|
remaining_arguments.remove(item)
|
||||||
|
|
||||||
arguments[canonical_name] = parsed
|
arguments[canonical_name] = parsed
|
||||||
|
@ -59,47 +65,33 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||||
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
# 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:
|
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
|
||||||
for subparser_name in ('prune', 'create', 'check'):
|
for subparser_name in ('prune', 'create', 'check'):
|
||||||
subparser = subparsers.choices[subparser_name]
|
subparser = subparsers[subparser_name]
|
||||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||||
arguments[subparser_name] = parsed
|
arguments[subparser_name] = parsed
|
||||||
|
|
||||||
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)
|
remaining_arguments = list(unparsed_arguments)
|
||||||
present_subparser_names = set()
|
|
||||||
|
|
||||||
for subparser_name, subparser in subparsers.choices.items():
|
# Now ask each subparser, one by one, to greedily consume arguments.
|
||||||
if subparser_name not in remaining_arguments:
|
for subparser_name, subparser in subparsers.items():
|
||||||
|
if subparser_name not in arguments.keys():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
present_subparser_names.add(subparser_name)
|
subparser = subparsers[subparser_name]
|
||||||
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||||
|
|
||||||
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
# Special case: If "borg" is present in the arguments, consume all arguments after (+1) the
|
||||||
if (
|
# "borg" action.
|
||||||
not present_subparser_names
|
if 'borg' in arguments:
|
||||||
and '--help' not in unparsed_arguments
|
borg_options_index = remaining_arguments.index('borg') + 1
|
||||||
and '-h' not in unparsed_arguments
|
arguments['borg'].options = remaining_arguments[borg_options_index:]
|
||||||
):
|
remaining_arguments = remaining_arguments[:borg_options_index]
|
||||||
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.
|
# Remove the subparser names themselves.
|
||||||
for subparser_name in present_subparser_names:
|
for subparser_name, subparser in subparsers.items():
|
||||||
if subparser_name in remaining_arguments:
|
if subparser_name in remaining_arguments:
|
||||||
remaining_arguments.remove(subparser_name)
|
remaining_arguments.remove(subparser_name)
|
||||||
|
|
||||||
return top_level_parser.parse_args(remaining_arguments)
|
return (arguments, remaining_arguments)
|
||||||
|
|
||||||
|
|
||||||
class Extend_action(Action):
|
class Extend_action(Action):
|
||||||
|
@ -510,8 +502,7 @@ def parse_arguments(*unparsed_arguments):
|
||||||
)
|
)
|
||||||
list_group = list_parser.add_argument_group('list arguments')
|
list_group = list_parser.add_argument_group('list arguments')
|
||||||
list_group.add_argument(
|
list_group.add_argument(
|
||||||
'--repository',
|
'--repository', help='Path of repository to list, defaults to the configured repositories',
|
||||||
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 (or "latest")')
|
list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
|
||||||
list_group.add_argument(
|
list_group.add_argument(
|
||||||
|
@ -601,8 +592,32 @@ def parse_arguments(*unparsed_arguments):
|
||||||
)
|
)
|
||||||
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
|
borg_parser = subparsers.add_parser(
|
||||||
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
|
'borg',
|
||||||
|
aliases=SUBPARSER_ALIASES['borg'],
|
||||||
|
help='Run an arbitrary Borg command',
|
||||||
|
description='Run an arbitrary Borg command based on borgmatic\'s configuration',
|
||||||
|
add_help=False,
|
||||||
|
)
|
||||||
|
borg_group = borg_parser.add_argument_group('borg arguments')
|
||||||
|
borg_group.add_argument(
|
||||||
|
'--repository',
|
||||||
|
help='Path of repository to pass to Borg, defaults to the configured repositories',
|
||||||
|
)
|
||||||
|
borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
|
||||||
|
borg_group.add_argument(
|
||||||
|
'--',
|
||||||
|
metavar='OPTION',
|
||||||
|
dest='options',
|
||||||
|
nargs='+',
|
||||||
|
help='Options to pass to Borg, command first ("create", "list", etc). "--" is optional. To specify the repository or the archive, you must use --repository or --archive instead of providing them here.',
|
||||||
|
)
|
||||||
|
borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||||
|
|
||||||
|
arguments, remaining_arguments = parse_subparser_arguments(
|
||||||
|
unparsed_arguments, subparsers.choices
|
||||||
|
)
|
||||||
|
arguments['global'] = top_level_parser.parse_args(remaining_arguments)
|
||||||
|
|
||||||
if arguments['global'].excludes_filename:
|
if arguments['global'].excludes_filename:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|
|
@ -9,6 +9,7 @@ from subprocess import CalledProcessError
|
||||||
import colorama
|
import colorama
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
|
from borgmatic.borg import borg as borg_borg
|
||||||
from borgmatic.borg import check as borg_check
|
from borgmatic.borg import check as borg_check
|
||||||
from borgmatic.borg import create as borg_create
|
from borgmatic.borg import create as borg_create
|
||||||
from borgmatic.borg import environment as borg_environment
|
from borgmatic.borg import environment as borg_environment
|
||||||
|
@ -543,6 +544,22 @@ def run_actions(
|
||||||
)
|
)
|
||||||
if json_output:
|
if json_output:
|
||||||
yield json.loads(json_output)
|
yield json.loads(json_output)
|
||||||
|
if 'borg' in arguments:
|
||||||
|
if arguments['borg'].repository is None or validate.repositories_match(
|
||||||
|
repository, arguments['borg'].repository
|
||||||
|
):
|
||||||
|
logger.warning('{}: Running arbitrary Borg command'.format(repository))
|
||||||
|
archive_name = borg_list.resolve_archive_name(
|
||||||
|
repository, arguments['borg'].archive, storage, local_path, remote_path
|
||||||
|
)
|
||||||
|
borg_borg.run_arbitrary_borg(
|
||||||
|
repository,
|
||||||
|
storage,
|
||||||
|
options=arguments['borg'].options,
|
||||||
|
archive=archive_name,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_configurations(config_filenames, overrides=None):
|
def load_configurations(config_filenames, overrides=None):
|
||||||
|
|
|
@ -3,7 +3,7 @@ FROM python:3.8-alpine3.12 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 mount umount restore list info; do \
|
&& for action in init prune create check extract export-tar mount umount restore list info borg; do \
|
||||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ title: How to develop on borgmatic
|
||||||
eleventyNavigation:
|
eleventyNavigation:
|
||||||
key: Develop on borgmatic
|
key: Develop on borgmatic
|
||||||
parent: How-to guides
|
parent: How-to guides
|
||||||
order: 11
|
order: 12
|
||||||
---
|
---
|
||||||
## Source code
|
## Source code
|
||||||
|
|
||||||
|
|
94
docs/how-to/run-arbitrary-borg-commands.md
Normal file
94
docs/how-to/run-arbitrary-borg-commands.md
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
---
|
||||||
|
title: How to run arbitrary Borg commands
|
||||||
|
eleventyNavigation:
|
||||||
|
key: Run arbitrary Borg commands
|
||||||
|
parent: How-to guides
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
|
## Running Borg with borgmatic
|
||||||
|
|
||||||
|
Borg has several commands (and options) that borgmatic does not currently
|
||||||
|
support. Sometimes though, as a borgmatic user, you may find yourself wanting
|
||||||
|
to take advantage of these off-the-beaten-path Borg features. You could of
|
||||||
|
course drop down to running Borg directly. But then you'd give up all the
|
||||||
|
niceties of your borgmatic configuration. You could file a [borgmatic
|
||||||
|
ticket](https://torsion.org/borgmatic/#issues) or even a [pull
|
||||||
|
request](https://torsion.org/borgmatic/#contributing) to add the feature. But
|
||||||
|
what if you need it *now*?
|
||||||
|
|
||||||
|
That's where borgmatic's support for running "arbitrary" Borg commands comes
|
||||||
|
in. Running Borg commands with borgmatic takes advantage of the following, all
|
||||||
|
based on your borgmatic configuration files or command-line arguments:
|
||||||
|
|
||||||
|
* configured repositories (automatically runs your Borg command once for each
|
||||||
|
one)
|
||||||
|
* local and remote Borg binary paths
|
||||||
|
* SSH settings and Borg environment variables
|
||||||
|
* lock wait settings
|
||||||
|
* verbosity
|
||||||
|
|
||||||
|
|
||||||
|
### borg action
|
||||||
|
|
||||||
|
The way you run Borg with borgmatic is via the `borg` action. Here's a simple
|
||||||
|
example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic borg break-lock
|
||||||
|
```
|
||||||
|
|
||||||
|
(No `borg` action in borgmatic? Time to upgrade!)
|
||||||
|
|
||||||
|
This runs Borg's `break-lock` command once on each configured borgmatic
|
||||||
|
repository. Notice how the repository isn't present in the specified Borg
|
||||||
|
options, as that part is provided by borgmatic.
|
||||||
|
|
||||||
|
You can also specify Borg options for relevant commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic borg list --progress
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs Borg's `list` command once on each configured borgmatic
|
||||||
|
repository. However, the native `borgmatic list` action should be preferred
|
||||||
|
for most use.
|
||||||
|
|
||||||
|
What if you only want to run Borg on a single configured borgmatic repository
|
||||||
|
when you've got several configured? Not a problem.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic borg --repository repo.borg break-lock
|
||||||
|
```
|
||||||
|
|
||||||
|
And what about a single archive?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic borg --archive your-archive-name list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limitations
|
||||||
|
|
||||||
|
borgmatic's `borg` action is not without limitations:
|
||||||
|
|
||||||
|
* The Borg command you want to run (`create`, `list`, etc.) *must* come first
|
||||||
|
after the `borg` action. If you have any other Borg options to specify,
|
||||||
|
provide them after. For instance, `borgmatic borg list --progress` will work,
|
||||||
|
but `borgmatic borg --progress list` will not.
|
||||||
|
* borgmatic supplies the repository/archive name to Borg for you (based on
|
||||||
|
your borgmatic configuration or the `borgmatic borg --repository`/`--archive`
|
||||||
|
arguments), so do not specify the repository/archive otherwise.
|
||||||
|
* The `borg` action will not currently work for any Borg commands like `borg
|
||||||
|
serve` that do not accept a repository/archive name.
|
||||||
|
* Do not specify any global borgmatic arguments to the right of the `borg`
|
||||||
|
action. (They will be passed to Borg instead of borgmatic.) If you have
|
||||||
|
global borgmatic arguments, specify them *before* the `borg` action.
|
||||||
|
* Unlike other borgmatic actions, you cannot combine the `borg` action with
|
||||||
|
other borgmatic actions. This is to prevent ambiguity in commands like
|
||||||
|
`borgmatic borg list`, in which `list` is both a valid Borg command and a
|
||||||
|
borgmatic action. In this case, only the Borg command is run.
|
||||||
|
* Unlike normal borgmatic actions that support JSON, the `borg` action will
|
||||||
|
not disable certain borgmatic logs to avoid interfering with JSON output.
|
||||||
|
|
||||||
|
In general, this `borgmatic borg` feature should be considered an escape
|
||||||
|
valve—a feature of second resort. In the long run, it's preferable to wrap
|
||||||
|
Borg commands with borgmatic actions that can support them fully.
|
|
@ -3,7 +3,7 @@ title: How to upgrade borgmatic
|
||||||
eleventyNavigation:
|
eleventyNavigation:
|
||||||
key: Upgrade borgmatic
|
key: Upgrade borgmatic
|
||||||
parent: How-to guides
|
parent: How-to guides
|
||||||
order: 10
|
order: 11
|
||||||
---
|
---
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
|
|
|
@ -163,6 +163,24 @@ def test_parse_arguments_with_help_and_action_shows_action_help(capsys):
|
||||||
assert 'create arguments:' in captured.out
|
assert 'create arguments:' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_action_before_global_options_parses_options():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('prune', '--verbosity', '2')
|
||||||
|
|
||||||
|
assert 'prune' in arguments
|
||||||
|
assert arguments['global'].verbosity == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_with_global_options_before_action_parses_options():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
arguments = module.parse_arguments('--verbosity', '2', 'prune')
|
||||||
|
|
||||||
|
assert 'prune' in arguments
|
||||||
|
assert arguments['global'].verbosity == 2
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
|
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
|
123
tests/unit/borg/test_borg.py
Normal file
123
tests/unit/borg/test_borg.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.borg import borg as module
|
||||||
|
|
||||||
|
from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_calls_borg_with_parameters():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['break-lock'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo', '--info'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['break-lock'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['break-lock'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||||
|
storage_config = {'lock_wait': 5}
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo', '--lock-wait', '5'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config=storage_config, options=['break-lock'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
|
||||||
|
storage_config = {}
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo::archive'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config=storage_config, options=['break-lock'], archive='archive',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg1', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1'
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['break-lock'], local_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['break-lock'], remote_path='borg1',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'list', 'repo', '--progress'),
|
||||||
|
output_log_level=logging.WARNING,
|
||||||
|
borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['list', '--progress'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=['--', 'break-lock'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
|
||||||
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
('borg',), output_log_level=logging.WARNING, borg_local_path='borg',
|
||||||
|
)
|
||||||
|
|
||||||
|
module.run_arbitrary_borg(
|
||||||
|
repository='repo', storage_config={}, options=[],
|
||||||
|
)
|
|
@ -5,146 +5,137 @@ from borgmatic.commands import arguments as module
|
||||||
|
|
||||||
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
|
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
|
||||||
action_namespace = flexmock(foo=True)
|
action_namespace = flexmock(foo=True)
|
||||||
subparsers = flexmock(
|
subparsers = {
|
||||||
choices={
|
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, ['action'])),
|
||||||
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
'other': flexmock(),
|
||||||
'other': flexmock(),
|
}
|
||||||
}
|
|
||||||
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
|
('--foo', 'true', 'action'), subparsers
|
||||||
)
|
)
|
||||||
|
|
||||||
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
|
|
||||||
|
|
||||||
assert arguments == {'action': action_namespace}
|
assert arguments == {'action': action_namespace}
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
|
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
|
||||||
action_namespace = flexmock(foo=True)
|
action_namespace = flexmock(foo=True)
|
||||||
subparsers = flexmock(
|
subparsers = {
|
||||||
choices={
|
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, ['action'])),
|
||||||
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
'other': flexmock(),
|
||||||
'other': flexmock(),
|
}
|
||||||
}
|
|
||||||
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
|
('action', '--foo', 'true'), subparsers
|
||||||
)
|
)
|
||||||
|
|
||||||
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), subparsers)
|
|
||||||
|
|
||||||
assert arguments == {'action': action_namespace}
|
assert arguments == {'action': action_namespace}
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
||||||
action_namespace = flexmock(foo=True)
|
action_namespace = flexmock(foo=True)
|
||||||
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
|
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, ['action']))
|
||||||
subparsers = flexmock(
|
subparsers = {
|
||||||
choices={
|
'action': action_subparser,
|
||||||
'action': action_subparser,
|
'-a': action_subparser,
|
||||||
'-a': action_subparser,
|
'other': flexmock(),
|
||||||
'other': flexmock(),
|
'-o': flexmock(),
|
||||||
'-o': flexmock(),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
|
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
|
||||||
|
|
||||||
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
|
('-a', '--foo', 'true'), subparsers
|
||||||
|
)
|
||||||
|
|
||||||
assert arguments == {'action': action_namespace}
|
assert arguments == {'action': action_namespace}
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
||||||
action_namespace = flexmock(foo=True)
|
action_namespace = flexmock(foo=True)
|
||||||
other_namespace = flexmock(bar=3)
|
other_namespace = flexmock(bar=3)
|
||||||
subparsers = flexmock(
|
subparsers = {
|
||||||
choices={
|
'action': flexmock(
|
||||||
'action': flexmock(
|
parse_known_args=lambda arguments: (action_namespace, ['action', '--bar', '3'])
|
||||||
parse_known_args=lambda arguments: (action_namespace, ['--bar', '3'])
|
),
|
||||||
),
|
'other': flexmock(parse_known_args=lambda arguments: (other_namespace, [])),
|
||||||
'other': flexmock(parse_known_args=lambda arguments: (other_namespace, [])),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments = module.parse_subparser_arguments(
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
|
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
|
||||||
)
|
)
|
||||||
|
|
||||||
assert arguments == {'action': action_namespace, 'other': other_namespace}
|
assert arguments == {'action': action_namespace, 'other': other_namespace}
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
||||||
|
|
||||||
def test_parse_subparser_arguments_applies_default_subparsers():
|
def test_parse_subparser_arguments_applies_default_subparsers():
|
||||||
prune_namespace = flexmock()
|
prune_namespace = flexmock()
|
||||||
create_namespace = flexmock(progress=True)
|
create_namespace = flexmock(progress=True)
|
||||||
check_namespace = flexmock()
|
check_namespace = flexmock()
|
||||||
subparsers = flexmock(
|
subparsers = {
|
||||||
choices={
|
'prune': flexmock(
|
||||||
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
|
parse_known_args=lambda arguments: (prune_namespace, ['prune', '--progress'])
|
||||||
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
|
),
|
||||||
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
|
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
|
||||||
'other': flexmock(),
|
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
|
||||||
}
|
'other': flexmock(),
|
||||||
)
|
}
|
||||||
|
|
||||||
arguments = module.parse_subparser_arguments(('--progress'), subparsers)
|
arguments, remaining_arguments = module.parse_subparser_arguments(('--progress'), subparsers)
|
||||||
|
|
||||||
assert arguments == {
|
assert arguments == {
|
||||||
'prune': prune_namespace,
|
'prune': prune_namespace,
|
||||||
'create': create_namespace,
|
'create': create_namespace,
|
||||||
'check': check_namespace,
|
'check': check_namespace,
|
||||||
}
|
}
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
||||||
|
|
||||||
def test_parse_global_arguments_with_help_does_not_apply_default_subparsers():
|
def test_parse_subparser_arguments_passes_through_unknown_arguments_before_subparser_name():
|
||||||
global_namespace = flexmock(verbosity='lots')
|
|
||||||
action_namespace = flexmock()
|
action_namespace = flexmock()
|
||||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
subparsers = {
|
||||||
subparsers = flexmock(
|
'action': flexmock(
|
||||||
choices={
|
parse_known_args=lambda arguments: (action_namespace, ['action', '--verbosity', 'lots'])
|
||||||
'action': flexmock(
|
),
|
||||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
'other': flexmock(),
|
||||||
),
|
}
|
||||||
'other': flexmock(),
|
|
||||||
}
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
|
('--verbosity', 'lots', 'action'), subparsers
|
||||||
)
|
)
|
||||||
|
|
||||||
arguments = module.parse_global_arguments(
|
assert arguments == {'action': action_namespace}
|
||||||
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
|
assert remaining_arguments == ['--verbosity', 'lots']
|
||||||
)
|
|
||||||
|
|
||||||
assert arguments == global_namespace
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_global_arguments_consumes_global_arguments_before_subparser_name():
|
def test_parse_subparser_arguments_passes_through_unknown_arguments_after_subparser_name():
|
||||||
global_namespace = flexmock(verbosity='lots')
|
|
||||||
action_namespace = flexmock()
|
action_namespace = flexmock()
|
||||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
subparsers = {
|
||||||
subparsers = flexmock(
|
'action': flexmock(
|
||||||
choices={
|
parse_known_args=lambda arguments: (action_namespace, ['action', '--verbosity', 'lots'])
|
||||||
'action': flexmock(
|
),
|
||||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
'other': flexmock(),
|
||||||
),
|
}
|
||||||
'other': flexmock(),
|
|
||||||
}
|
arguments, remaining_arguments = module.parse_subparser_arguments(
|
||||||
|
('action', '--verbosity', 'lots'), subparsers
|
||||||
)
|
)
|
||||||
|
|
||||||
arguments = module.parse_global_arguments(
|
assert arguments == {'action': action_namespace}
|
||||||
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
|
assert remaining_arguments == ['--verbosity', 'lots']
|
||||||
)
|
|
||||||
|
|
||||||
assert arguments == global_namespace
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_global_arguments_consumes_global_arguments_after_subparser_name():
|
def test_parse_subparser_arguments_parses_borg_options_and_skips_other_subparsers():
|
||||||
global_namespace = flexmock(verbosity='lots')
|
action_namespace = flexmock(options=[])
|
||||||
action_namespace = flexmock()
|
subparsers = {
|
||||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
'borg': flexmock(parse_known_args=lambda arguments: (action_namespace, ['borg', 'list'])),
|
||||||
subparsers = flexmock(
|
'list': flexmock(),
|
||||||
choices={
|
}
|
||||||
'action': flexmock(
|
|
||||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
|
||||||
),
|
|
||||||
'other': flexmock(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
arguments = module.parse_global_arguments(
|
arguments, remaining_arguments = module.parse_subparser_arguments(('borg', 'list'), subparsers)
|
||||||
('action', '--verbosity', 'lots'), top_level_parser, subparsers
|
|
||||||
)
|
|
||||||
|
|
||||||
assert arguments == global_namespace
|
assert arguments == {'borg': action_namespace}
|
||||||
|
assert arguments['borg'].options == ['list']
|
||||||
|
assert remaining_arguments == []
|
||||||
|
|
Loading…
Reference in a new issue