Support for Borg 2's rcreate and rinfo sub-commands (#557).

This commit is contained in:
Dan Helfman 2022-08-12 14:53:20 -07:00
parent 22149c6401
commit 622caa0c21
18 changed files with 434 additions and 239 deletions

3
NEWS
View file

@ -1,4 +1,5 @@
1.6.7.dev0
2.0.0.dev0
* #557: Support for Borg 2 while still working with Borg 1.
* #565: Fix handling of "repository" and "data" consistency checks to prevent invalid Borg flags.
* #566: Modify "mount" and "extract" actions to require the "--repository" flag when multiple
repositories are configured.

View file

@ -5,7 +5,7 @@ import logging
import os
import pathlib
from borgmatic.borg import environment, extract, info, state
from borgmatic.borg import environment, extract, rinfo, state
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
DEFAULT_CHECKS = (
@ -241,6 +241,7 @@ def check_archives(
location_config,
storage_config,
consistency_config,
local_borg_version,
local_path='borg',
remote_path=None,
progress=None,
@ -260,10 +261,11 @@ def check_archives(
'''
try:
borg_repository_id = json.loads(
info.display_archives_info(
rinfo.display_repository_info(
repository,
storage_config,
argparse.Namespace(json=True, archive=None),
local_borg_version,
argparse.Namespace(json=True),
local_path,
remote_path,
)

View file

@ -9,6 +9,10 @@ class Feature(Enum):
NOFLAGS = 3
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
SEPARATE_REPOSITORY_ARCHIVE = 6
RCREATE = 7
RLIST = 8
RINFO = 9
FEATURE_TO_MINIMUM_BORG_VERSION = {
@ -17,6 +21,10 @@ FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.NOFLAGS: parse_version('1.2.0a8'), # borg create --noflags
Feature.NUMERIC_IDS: parse_version('1.2.0b3'), # borg create/extract/mount --numeric-ids
Feature.UPLOAD_RATELIMIT: parse_version('1.2.0b3'), # borg create --upload-ratelimit
Feature.SEPARATE_REPOSITORY_ARCHIVE: parse_version('2.0.0a2'), # --repo with separate archive
Feature.RCREATE: parse_version('2.0.0a2'), # borg rcreate
Feature.RLIST: parse_version('2.0.0a2'), # borg rlist
Feature.RINFO: parse_version('2.0.0a2'), # borg rinfo
}

View file

@ -1,6 +1,6 @@
import logging
from borgmatic.borg import environment
from borgmatic.borg import environment, feature
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
from borgmatic.execute import execute_command
@ -8,12 +8,17 @@ logger = logging.getLogger(__name__)
def display_archives_info(
repository, storage_config, info_arguments, local_path='borg', remote_path=None
repository,
storage_config,
local_borg_version,
info_arguments,
local_path='borg',
remote_path=None,
):
'''
Given a local or remote repository path, a storage config dict, and the arguments to the info
action, display summary information for Borg archives in the repository or return JSON summary
information.
Given a local or remote repository path, a storage config dict, the local Borg version, and the
arguments to the info action, display summary information for Borg archives in the repository or
return JSON summary information.
'''
lock_wait = storage_config.get('lock_wait', None)
@ -33,11 +38,18 @@ def display_archives_info(
+ make_flags('lock-wait', lock_wait)
+ make_flags_from_arguments(info_arguments, excludes=('repository', 'archive'))
+ (
(
('--repo', repository)
+ (('--glob-archives', info_arguments.archive) if info_arguments.archive else ())
)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else (
'::'.join((repository, info_arguments.archive))
if info_arguments.archive
else repository,
)
)
)
return execute_command(
full_command,

View file

@ -2,18 +2,19 @@ import argparse
import logging
import subprocess
from borgmatic.borg import environment, info
from borgmatic.borg import environment, feature, rinfo
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def initialize_repository(
def create_repository(
repository,
storage_config,
local_borg_version,
encryption_mode,
append_only=None,
storage_quota=None,
@ -21,28 +22,34 @@ def initialize_repository(
remote_path=None,
):
'''
Given a local or remote repository path, a storage configuration dict, a Borg encryption mode,
whether the repository should be append-only, and the storage quota to use, initialize the
repository. If the repository already exists, then log and skip initialization.
Given a local or remote repository path, a storage configuration dict, the local Borg version, a
Borg encryption mode, whether the repository should be append-only, and the storage quota to
use, create the repository. If the repository already exists, then log and skip creation.
'''
try:
info.display_archives_info(
rinfo.display_repository_info(
repository,
storage_config,
argparse.Namespace(json=True, archive=None),
local_borg_version,
argparse.Namespace(json=True),
local_path,
remote_path,
)
logger.info('Repository already exists. Skipping initialization.')
logger.info('Repository already exists. Skipping creation.')
return
except subprocess.CalledProcessError as error:
if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
if error.returncode != RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
raise
extra_borg_options = storage_config.get('extra_borg_options', {}).get('init', '')
extra_borg_options = storage_config.get('extra_borg_options', {}).get('rcreate', '')
init_command = (
(local_path, 'init')
rcreate_command = (
(local_path,)
+ (
('rcreate',)
if feature.available(feature.Feature.RCREATE, local_borg_version)
else ('init',)
)
+ (('--encryption', encryption_mode) if encryption_mode else ())
+ (('--append-only',) if append_only else ())
+ (('--storage-quota', storage_quota) if storage_quota else ())
@ -50,12 +57,17 @@ def initialize_repository(
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (
('--repo',)
if feature.available(feature.Feature.SEPARATE_REPOSITORY_ARCHIVE, local_borg_version)
else ()
)
+ (repository,)
)
# Do not capture output here, so as to support interactive prompts.
execute_command(
init_command,
rcreate_command,
output_file=DO_NOT_CAPTURE,
borg_local_path=local_path,
extra_environment=environment.make_environment(storage_config),

View file

@ -4,7 +4,7 @@ from argparse import Action, ArgumentParser
from borgmatic.config import collect
SUBPARSER_ALIASES = {
'init': ['--init', '-I'],
'rcreate': ['init', '--init', '-I'],
'prune': ['--prune', '-p'],
'compact': [],
'create': ['--create', '-C'],
@ -222,33 +222,35 @@ def make_parsers():
metavar='',
help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:',
)
init_parser = subparsers.add_parser(
'init',
aliases=SUBPARSER_ALIASES['init'],
help='Initialize an empty Borg repository',
description='Initialize an empty Borg repository',
rcreate_parser = subparsers.add_parser(
'rcreate',
aliases=SUBPARSER_ALIASES['rcreate'],
help='Create a new, empty Borg repository',
description='Create a new, empty Borg repository',
add_help=False,
)
init_group = init_parser.add_argument_group('init arguments')
init_group.add_argument(
rcreate_group = rcreate_parser.add_argument_group('rcreate arguments')
rcreate_group.add_argument(
'-e',
'--encryption',
dest='encryption_mode',
help='Borg repository encryption mode',
required=True,
)
init_group.add_argument(
rcreate_group.add_argument(
'--append-only',
dest='append_only',
action='store_true',
help='Create an append-only repository',
)
init_group.add_argument(
rcreate_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')
rcreate_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
prune_parser = subparsers.add_parser(
'prune',
@ -688,11 +690,11 @@ def parse_arguments(*unparsed_arguments):
if arguments['global'].excludes_filename:
raise ValueError(
'The --excludes option has been replaced with exclude_patterns in configuration'
'The --excludes flag 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 'rcreate' in arguments and arguments['global'].dry_run:
raise ValueError('The rcreate/init action cannot be used with the --dry-run flag')
if (
'list' in arguments
@ -700,6 +702,11 @@ def parse_arguments(*unparsed_arguments):
and arguments['list'].json
and arguments['info'].json
):
raise ValueError('With the --json option, list and info actions cannot be used together')
raise ValueError('With the --json flag, list and info actions cannot be used together')
if 'info' in arguments and arguments['info'].archive and arguments['info'].glob_archives:
raise ValueError(
'With the info action, the --archive and --glob-archives flags cannot be used together'
)
return arguments

View file

@ -20,10 +20,10 @@ from borgmatic.borg import export_tar as borg_export_tar
from borgmatic.borg import extract as borg_extract
from borgmatic.borg import feature as borg_feature
from borgmatic.borg import info as borg_info
from borgmatic.borg import init as borg_init
from borgmatic.borg import list as borg_list
from borgmatic.borg import mount as borg_mount
from borgmatic.borg import prune as borg_prune
from borgmatic.borg import rcreate as borg_rcreate
from borgmatic.borg import umount as borg_umount
from borgmatic.borg import version as borg_version
from borgmatic.commands.arguments import parse_arguments
@ -249,14 +249,15 @@ def run_actions(
'repositories': ','.join(location['repositories']),
}
if 'init' in arguments:
logger.info('{}: Initializing repository'.format(repository))
borg_init.initialize_repository(
if 'rcreate' in arguments:
logger.info('{}: Creating repository'.format(repository))
borg_rcreate.create_repository(
repository,
storage,
arguments['init'].encryption_mode,
arguments['init'].append_only,
arguments['init'].storage_quota,
local_borg_version,
arguments['rcreate'].encryption_mode,
arguments['rcreate'].append_only,
arguments['rcreate'].storage_quota,
local_path=local_path,
remote_path=remote_path,
)
@ -396,6 +397,7 @@ def run_actions(
location,
storage,
consistency,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
progress=arguments['check'].progress,
@ -624,6 +626,7 @@ def run_actions(
json_output = borg_info.display_archives_info(
repository,
storage,
local_borg_version,
info_arguments=info_arguments,
local_path=local_path,
remote_path=remote_path,

View file

@ -27,9 +27,6 @@ borgmatic create
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 actions provided, or you can mix and match
any number of them in a single borgmatic run. This supports approaches like
skipping certain actions while running others. For instance, this skips
@ -70,7 +67,9 @@ Here are the available checks from fastest to slowest:
* `extract`: Performs an extraction dry-run of the most recent archive.
* `data`: Verifies the data integrity of all archives contents, decrypting and decompressing all data (implies `archives` as well).
See [Borg's check documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html) for more information.
See [Borg's check
documentation](https://borgbackup.readthedocs.io/en/stable/usage/check.html)
for more information.
### Check frequency

View file

@ -37,19 +37,22 @@ borgmatic --stats
## Existing backups
borgmatic provides convenient actions for Borg's
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
[`list`](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
[`info`](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
functionality:
```bash
borgmatic list
borgmatic info
```
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
`--info`. Or upgrade borgmatic!)
<span class="minilink minilink-addedin">New in borgmatic version 2.0.0</span>
There's also an `rinfo` action for displaying repository information with Borg
2.x:
```bash
borgmatic rinfo
```
### Searching for a file

View file

@ -186,32 +186,36 @@ files via configuration management, or you want to double check that your hand
edits are valid.
## Initialization
## Repository creation
Before you can create backups with borgmatic, you first need to initialize a
Borg repository so you have a destination for your backup archives. (But skip
this step if you already have a Borg repository.) To create a repository, run
a command like the following:
Before you can create backups with borgmatic, you first need to create a Borg
repository so you have a destination for your backup archives. (But skip this
step if you already have a Borg repository.) To create a repository, run a
command like the following with Borg 1.x:
```bash
sudo borgmatic init --encryption repokey
```
(No borgmatic `init` action? Try the old-style `--init` flag, or upgrade
borgmatic!)
<span class="minilink minilink-addedin">New in borgmatic version 2.0.0</span>
Or, with Borg 2.x:
```bash
sudo borgmatic rcreate --encryption repokey-aes-ocb
```
This uses the borgmatic configuration file you created above to determine
which local or remote repository to create, and encrypts it with the
encryption passphrase specified there if one is provided. Read about [Borg
encryption
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-modes)
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-mode-tldr)
for the menu of available encryption modes.
Also, optionally check out the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/stable/quickstart.html) for more
background about repository initialization.
background about repository creation.
Note that borgmatic skips repository initialization if the repository already
Note that borgmatic skips repository creation if the repository already
exists. This supports use cases like ensuring a repository exists prior to
performing a backup.
@ -221,8 +225,8 @@ key-based SSH access to the desired user account on the remote host.
## Backups
Now that you've configured borgmatic and initialized a repository, it's a
good idea to test that borgmatic is working. So to run borgmatic and start a
Now that you've configured borgmatic and created a repository, it's a good
idea to test that borgmatic is working. So to run borgmatic and start a
backup, you can invoke it like this:
```bash
@ -230,7 +234,7 @@ sudo borgmatic create --verbosity 1 --files --stats
```
(No borgmatic `--files` flag? It's only present in newer versions of
borgmatic. So try leaving it out, or upgrade borgmatic!)
borgmatic. So try leaving it out or upgrade borgmatic!)
The `--verbosity` flag makes borgmatic show the steps it's performing. The
`--files` flag lists each file that's new or changed since the last backup.

View file

@ -14,8 +14,8 @@ apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client m
py3-ruamel.yaml py3-ruamel.yaml.clib bash
# If certain dependencies of black are available in this version of Alpine, install them.
apk add --no-cache py3-typed-ast py3-regex || true
python3 -m pip install --no-cache --upgrade pip==22.0.3 setuptools==60.8.1
pip3 install tox==3.24.5
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1
pip3 install --ignore-installed tox==3.25.1
export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox --sitepackages
tox --workdir /tmp/.tox --sitepackages -e end-to-end

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.6.7.dev0'
VERSION = '2.0.0.dev0'
setup(

View file

@ -496,6 +496,13 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
module.parse_arguments('list', 'info', '--json')
def test_parse_arguments_disallows_info_with_both_archive_and_glob_archives():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
with pytest.raises(ValueError):
module.parse_arguments('info', '--archive', 'foo', '--glob-archives', '*bar')
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'])

View file

@ -296,7 +296,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
@ -315,6 +315,7 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
progress=True,
)
@ -324,7 +325,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
@ -343,6 +344,7 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
repair=True,
)
@ -361,7 +363,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
@ -376,6 +378,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -385,7 +388,7 @@ def test_check_archives_with_json_error_raises():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"unexpected": {"id": "repo"}}'
)
@ -395,6 +398,7 @@ def test_check_archives_with_json_error_raises():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -404,7 +408,7 @@ def test_check_archives_with_missing_json_keys_raises():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return('{invalid JSON')
flexmock(module.rinfo).should_receive('display_repository_info').and_return('{invalid JSON')
with pytest.raises(ValueError):
module.check_archives(
@ -412,6 +416,7 @@ def test_check_archives_with_missing_json_keys_raises():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -421,7 +426,7 @@ def test_check_archives_with_extract_check_calls_extract_only():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').never()
@ -434,6 +439,7 @@ def test_check_archives_with_extract_check_calls_extract_only():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -442,7 +448,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
@ -456,6 +462,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -464,7 +471,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
@ -478,6 +485,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -485,7 +493,7 @@ def test_check_archives_without_any_checks_bails():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(())
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
insert_execute_command_never()
@ -495,6 +503,7 @@ def test_check_archives_without_any_checks_bails():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -504,7 +513,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
@ -519,6 +528,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
local_path='borg1',
)
@ -529,7 +539,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
@ -544,6 +554,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
remote_path='borg1',
)
@ -554,7 +565,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
consistency_config = {'check_last': check_last}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
@ -569,6 +580,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
location_config={},
storage_config={'lock_wait': 5},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -579,7 +591,7 @@ def test_check_archives_with_retention_prefix():
consistency_config = {'check_last': check_last, 'prefix': prefix}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').with_args(
@ -594,6 +606,7 @@ def test_check_archives_with_retention_prefix():
location_config={},
storage_config={},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)
@ -602,7 +615,7 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
consistency_config = {'check_last': None}
flexmock(module).should_receive('parse_checks')
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
flexmock(module.info).should_receive('display_archives_info').and_return(
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
'{"repository": {"id": "repo"}}'
)
flexmock(module).should_receive('make_check_flags').and_return(())
@ -615,4 +628,5 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
location_config={},
storage_config={'extra_borg_options': {'check': '--extra --options'}},
consistency_config=consistency_config,
local_borg_version='1.2.3',
)

View file

@ -9,6 +9,25 @@ from ..test_verbosity import insert_logging_mock
def test_display_archives_info_calls_borg_with_parameters():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
)
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
)
def test_display_archives_info_without_borg_features_calls_borg_without_repo_flag():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', 'repo'),
@ -18,28 +37,36 @@ def test_display_archives_info_calls_borg_with_parameters():
)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
)
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--info', 'repo'),
('borg', 'info', '--info', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
)
insert_logging_mock(logging.INFO)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
)
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
@ -47,16 +74,20 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
insert_logging_mock(logging.INFO)
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True),
)
assert json_output == '[]'
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--debug', '--show-rc', 'repo'),
('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
@ -64,14 +95,18 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
insert_logging_mock(logging.DEBUG)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
)
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
@ -79,29 +114,55 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
insert_logging_mock(logging.DEBUG)
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True),
)
assert json_output == '[]'
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--json', 'repo'),
('borg', 'info', '--json', '--repo', 'repo'),
output_log_level=None,
borg_local_path='borg',
extra_environment=None,
).and_return('[]')
json_output = module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=True),
)
assert json_output == '[]'
def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
def test_display_archives_info_with_archive_calls_borg_with_glob_archives_parameter():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--repo', 'repo', '--glob-archives', 'archive'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
)
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive='archive', json=False),
)
def test_display_archives_info_with_archive_and_without_borg_features_calls_borg_with_repo_archive_parameter():
flexmock(module.feature).should_receive('available').and_return(False)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', 'repo::archive'),
@ -111,14 +172,18 @@ def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
)
module.display_archives_info(
repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False)
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive='archive', json=False),
)
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg1', 'info', 'repo'),
('borg1', 'info', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg1',
extra_environment=None,
@ -127,15 +192,17 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
local_path='borg1',
)
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--remote-path', 'borg1', 'repo'),
('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
@ -144,6 +211,7 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False),
remote_path='borg1',
)
@ -151,9 +219,10 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--lock-wait', '5', 'repo'),
('borg', 'info', '--lock-wait', '5', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
@ -162,15 +231,17 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
module.display_archives_info(
repository='repo',
storage_config=storage_config,
local_borg_version='2.3.4',
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.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', '--repo', 'repo'),
output_log_level=logging.WARNING,
borg_local_path='borg',
extra_environment=None,
@ -179,5 +250,6 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
module.display_archives_info(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}),
)

View file

@ -1,132 +0,0 @@
import logging
import subprocess
import pytest
from flexmock import flexmock
from borgmatic.borg import init as module
from ..test_verbosity import insert_logging_mock
INFO_SOME_UNKNOWN_EXIT_CODE = -999
INIT_COMMAND = ('borg', 'init', '--encryption', 'repokey')
def insert_info_command_found_mock():
flexmock(module.info).should_receive('display_archives_info')
def insert_info_command_not_found_mock():
flexmock(module.info).should_receive('display_archives_info').and_raise(
subprocess.CalledProcessError(module.INFO_REPOSITORY_NOT_FOUND_EXIT_CODE, [])
)
def insert_init_command_mock(init_command, **kwargs):
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
init_command,
output_file=module.DO_NOT_CAPTURE,
borg_local_path=init_command[0],
extra_environment=None,
).once()
def test_initialize_repository_calls_borg_with_parameters():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('repo',))
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_raises_for_borg_init_error():
insert_info_command_not_found_mock()
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').and_raise(
module.subprocess.CalledProcessError(2, 'borg init')
)
with pytest.raises(subprocess.CalledProcessError):
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey'
)
def test_initialize_repository_skips_initialization_when_repository_already_exists():
insert_info_command_found_mock()
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_raises_for_unknown_info_command_error():
flexmock(module.info).should_receive('display_archives_info').and_raise(
subprocess.CalledProcessError(INFO_SOME_UNKNOWN_EXIT_CODE, [])
)
with pytest.raises(subprocess.CalledProcessError):
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey'
)
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--append-only', 'repo'))
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey', append_only=True
)
def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G', 'repo'))
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey', storage_quota='5G'
)
def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--info', 'repo'))
insert_logging_mock(logging.INFO)
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo'))
insert_logging_mock(logging.DEBUG)
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
def test_initialize_repository_with_local_path_calls_borg_via_local_path():
insert_info_command_not_found_mock()
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:] + ('repo',))
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey', local_path='borg1'
)
def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1', 'repo'))
module.initialize_repository(
repository='repo', storage_config={}, encryption_mode='repokey', remote_path='borg1'
)
def test_initialize_repository_with_extra_borg_options_calls_borg_with_extra_options():
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--extra', '--options', 'repo'))
module.initialize_repository(
repository='repo',
storage_config={'extra_borg_options': {'init': '--extra --options'}},
encryption_mode='repokey',
)

View file

@ -0,0 +1,183 @@
import logging
import subprocess
import pytest
from flexmock import flexmock
from borgmatic.borg import rcreate as module
from ..test_verbosity import insert_logging_mock
RINFO_SOME_UNKNOWN_EXIT_CODE = -999
RCREATE_COMMAND = ('borg', 'rcreate', '--encryption', 'repokey')
def insert_rinfo_command_found_mock():
flexmock(module.rinfo).should_receive('display_repository_info')
def insert_rinfo_command_not_found_mock():
flexmock(module.rinfo).should_receive('display_repository_info').and_raise(
subprocess.CalledProcessError(module.RINFO_REPOSITORY_NOT_FOUND_EXIT_CODE, [])
)
def insert_rcreate_command_mock(rcreate_command, **kwargs):
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').with_args(
rcreate_command,
output_file=module.DO_NOT_CAPTURE,
borg_local_path=rcreate_command[0],
extra_environment=None,
).once()
def test_create_repository_calls_borg_with_parameters():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey'
)
def test_create_repository_without_borg_features_calls_borg_with_init_sub_command():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(('borg', 'init', '--encryption', 'repokey', 'repo'))
flexmock(module.feature).should_receive('available').and_return(False)
module.create_repository(
repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey'
)
def test_create_repository_raises_for_borg_rcreate_error():
insert_rinfo_command_not_found_mock()
flexmock(module.feature).should_receive('available').and_return(True)
flexmock(module.environment).should_receive('make_environment')
flexmock(module).should_receive('execute_command').and_raise(
module.subprocess.CalledProcessError(2, 'borg rcreate')
)
with pytest.raises(subprocess.CalledProcessError):
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
)
def test_create_repository_skips_creation_when_repository_already_exists():
insert_rinfo_command_found_mock()
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey'
)
def test_create_repository_raises_for_unknown_rinfo_command_error():
flexmock(module.rinfo).should_receive('display_repository_info').and_raise(
subprocess.CalledProcessError(RINFO_SOME_UNKNOWN_EXIT_CODE, [])
)
with pytest.raises(subprocess.CalledProcessError):
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
)
def test_create_repository_with_append_only_calls_borg_with_append_only_parameter():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--append-only', '--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
append_only=True,
)
def test_create_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--storage-quota', '5G', '--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
storage_quota='5G',
)
def test_create_repository_with_log_info_calls_borg_with_info_parameter():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--info', '--repo', 'repo'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey'
)
def test_create_repository_with_log_debug_calls_borg_with_debug_parameter():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--debug', '--repo', 'repo'))
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo', storage_config={}, local_borg_version='2.3.4', encryption_mode='repokey'
)
def test_create_repository_with_local_path_calls_borg_via_local_path():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(('borg1',) + RCREATE_COMMAND[1:] + ('--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
local_path='borg1',
)
def test_create_repository_with_remote_path_calls_borg_with_remote_path_parameter():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--remote-path', 'borg1', '--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo',
storage_config={},
local_borg_version='2.3.4',
encryption_mode='repokey',
remote_path='borg1',
)
def test_create_repository_with_extra_borg_options_calls_borg_with_extra_options():
insert_rinfo_command_not_found_mock()
insert_rcreate_command_mock(RCREATE_COMMAND + ('--extra', '--options', '--repo', 'repo'))
flexmock(module.feature).should_receive('available').and_return(True)
module.create_repository(
repository='repo',
storage_config={'extra_borg_options': {'rcreate': '--extra --options'}},
local_borg_version='2.3.4',
encryption_mode='repokey',
)

View file

@ -340,11 +340,11 @@ def test_run_configuration_retries_timeout_multiple_repos():
assert results == error_logs
def test_run_actions_does_not_raise_for_init_action():
flexmock(module.borg_init).should_receive('initialize_repository')
def test_run_actions_does_not_raise_for_rcreate_action():
flexmock(module.borg_rcreate).should_receive('create_repository')
arguments = {
'global': flexmock(monitoring_verbosity=1, dry_run=False),
'init': flexmock(
'rcreate': flexmock(
encryption_mode=flexmock(), append_only=flexmock(), storage_quota=flexmock()
),
}