mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-24 19:03:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bea7ae5ed | ||
|
|
23679a6edd | ||
|
|
0f44fbedf4 | ||
|
|
ae15e0f404 | ||
|
|
a2e8abc537 | ||
|
|
10404143c6 | ||
|
|
94aaf4554f | ||
|
|
0c8816e6cc | ||
|
|
e3e4aeff94 | ||
|
|
89cd879529 |
@@ -32,3 +32,6 @@ de2d7721cdec93a52d20222a9ddd579ed93c1017 1.0.1
|
||||
32c6341dda9fad77a3982641bce8a3a45821842e 1.0.3
|
||||
5a003056a8ff4709c5bd4d6d33354199423f8a1d 1.1.0
|
||||
7d3d11eff6c0773883c48f221431f157bc7995eb 1.1.1
|
||||
f052a77a8ad5a0fea7fa86a902e0e401252f7d80 1.1.2
|
||||
3f838f661546e04529b453aa443529b432afc243 1.1.3
|
||||
3d605962d891731a0f372b903b556ac7a8c8359f 1.1.4
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
1.1.5
|
||||
|
||||
* #34: New "extract" consistency check that performs a dry-run extraction of the most recent
|
||||
archive.
|
||||
|
||||
1.1.4
|
||||
|
||||
* #17: Added command-line flags for performing a borgmatic run with only pruning, creating, or
|
||||
checking enabled. This supports use cases like running consistency checks from a different cron
|
||||
job with a different frequency, or running pruning with a different verbosity level.
|
||||
|
||||
1.1.3
|
||||
|
||||
* #14: Support for running multiple config files in /etc/borgmatic.d/ from a single borgmatic run.
|
||||
* Fix for generate-borgmatic-config writing config with invalid one_file_system value.
|
||||
|
||||
1.1.2
|
||||
|
||||
* #32: Fix for passing check_last as integer to subprocess when calling Borg.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
title: borgmatic
|
||||
|
||||
<img src="static/borgmatic.svg" alt="borgmatic logo" style="width: 8em; float: right; padding-left: 1em;" />
|
||||
<img src="https://cdn.rawgit.com/witten/borgmatic/master/static/borgmatic.svg" width="150px" style="float: right; padding-left: 1em;">
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -68,7 +66,9 @@ To install borgmatic, run the following command to download and install it:
|
||||
Make sure you're using Python 3, as borgmatic does not support Python 2. (You
|
||||
may have to use "pip3" or similar instead of "pip".)
|
||||
|
||||
Then, generate a sample configuration file:
|
||||
## Configuration
|
||||
|
||||
After you install borgmatic, generate a sample configuration file:
|
||||
|
||||
sudo generate-borgmatic-config
|
||||
|
||||
@@ -77,6 +77,31 @@ default). You should edit the file to suit your needs, as the values are just
|
||||
representative. All fields are optional except where indicated, so feel free
|
||||
to remove anything you don't need.
|
||||
|
||||
You can also have a look at the [full configuration
|
||||
schema](https://torsion.org/hg/borgmatic/file/tip/borgmatic/config/schema.yaml)
|
||||
for the authoritative set of all configuration options. This is handy if
|
||||
borgmatic has added new options since you originally created your
|
||||
configuration file.
|
||||
|
||||
|
||||
### Multiple configuration files
|
||||
|
||||
A more advanced usage is to create multiple separate configuration files and
|
||||
place each one in an /etc/borgmatic.d directory. For instance:
|
||||
|
||||
sudo mkdir /etc/borgmatic.d
|
||||
sudo generate-borgmatic-config --destination /etc/borgmatic.d/app1.yaml
|
||||
sudo generate-borgmatic-config --destination /etc/borgmatic.d/app2.yaml
|
||||
|
||||
With this approach, you can have entirely different backup policies for
|
||||
different applications on your system. For instance, you may want one backup
|
||||
configuration for your database data directory, and a different configuration
|
||||
for your user home directories.
|
||||
|
||||
When you set up multiple configuration files like this, borgmatic will run
|
||||
each one in turn from a single borgmatic invocation. This includes, by
|
||||
default, the traditional /etc/borgmatic/config.yaml as well.
|
||||
|
||||
|
||||
## Upgrading
|
||||
|
||||
@@ -154,6 +179,12 @@ arguments:
|
||||
This will also prune any old backups as per the configured retention policy,
|
||||
and check backups for consistency problems due to things like file damage.
|
||||
|
||||
If you'd like to see the available command-line arguments, view the help:
|
||||
|
||||
borgmatic --help
|
||||
|
||||
### Verbosity
|
||||
|
||||
By default, the backup will proceed silently except in the case of errors. But
|
||||
if you'd like to to get additional information about the progress of the
|
||||
backup as it proceeds, use the verbosity option:
|
||||
@@ -164,9 +195,19 @@ Or, for even more progress spew:
|
||||
|
||||
borgmatic --verbosity 2
|
||||
|
||||
If you'd like to see the available command-line arguments, view the help:
|
||||
### À la carte
|
||||
|
||||
borgmatic --help
|
||||
If you want to run borgmatic with only pruning, creating, or checking enabled,
|
||||
the following optional flags are available:
|
||||
|
||||
borgmatic --prune
|
||||
borgmatic --create
|
||||
borgmatic --check
|
||||
|
||||
You can run with only one of these flags provided, or you can mix and match
|
||||
any number of them. This supports use cases like running consistency checks
|
||||
from a different cron job with a different frequency, or running pruning with
|
||||
a different verbosity level.
|
||||
|
||||
|
||||
## Autopilot
|
||||
|
||||
+53
-13
@@ -3,6 +3,7 @@ import glob
|
||||
import itertools
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -43,7 +44,7 @@ def create_archive(
|
||||
):
|
||||
'''
|
||||
Given a vebosity flag, a storage config dict, a list of source directories, a local or remote
|
||||
repository path, a list of exclude patterns, and a command to run, create an attic archive.
|
||||
repository path, a list of exclude patterns, and a command to run, create a Borg archive.
|
||||
'''
|
||||
sources = tuple(
|
||||
itertools.chain.from_iterable(
|
||||
@@ -68,8 +69,8 @@ def create_archive(
|
||||
|
||||
full_command = (
|
||||
command, 'create',
|
||||
'{repo}::{hostname}-{timestamp}'.format(
|
||||
repo=repository,
|
||||
'{repository}::{hostname}-{timestamp}'.format(
|
||||
repository=repository,
|
||||
hostname=platform.node(),
|
||||
timestamp=datetime.now().isoformat(),
|
||||
),
|
||||
@@ -104,7 +105,7 @@ def _make_prune_flags(retention_config):
|
||||
def prune_archives(verbosity, repository, retention_config, command=COMMAND, remote_path=None):
|
||||
'''
|
||||
Given a verbosity flag, a local or remote repository path, a retention config dict, and a
|
||||
command to run, prune attic archives according the the retention policy specified in that
|
||||
command to run, prune Borg archives according the the retention policy specified in that
|
||||
configuration.
|
||||
'''
|
||||
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
|
||||
@@ -170,33 +171,72 @@ def _make_check_flags(checks, check_last=None):
|
||||
|
||||
return tuple(
|
||||
'--{}-only'.format(check) for check in checks
|
||||
if check in DEFAULT_CHECKS
|
||||
) + last_flag
|
||||
|
||||
|
||||
def check_archives(verbosity, repository, consistency_config, command=COMMAND, remote_path=None):
|
||||
'''
|
||||
Given a verbosity flag, a local or remote repository path, a consistency config dict, and a
|
||||
command to run, check the contained attic archives for consistency.
|
||||
command to run, check the contained Borg archives for consistency.
|
||||
|
||||
If there are no consistency checks to run, skip running them.
|
||||
'''
|
||||
checks = _parse_checks(consistency_config)
|
||||
check_last = consistency_config.get('check_last', None)
|
||||
if not checks:
|
||||
return
|
||||
|
||||
if set(checks).intersection(set(DEFAULT_CHECKS)):
|
||||
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
|
||||
verbosity_flags = {
|
||||
VERBOSITY_SOME: ('--info',),
|
||||
VERBOSITY_LOTS: ('--debug',),
|
||||
}.get(verbosity, ())
|
||||
|
||||
full_command = (
|
||||
command, 'check',
|
||||
repository,
|
||||
) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
|
||||
|
||||
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
|
||||
stdout = None if verbosity_flags else open(os.devnull, 'w')
|
||||
|
||||
subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
|
||||
|
||||
if 'extract' in checks:
|
||||
extract_last_archive_dry_run(verbosity, repository, command, remote_path)
|
||||
|
||||
|
||||
def extract_last_archive_dry_run(verbosity, repository, command=COMMAND, remote_path=None):
|
||||
'''
|
||||
Perform an extraction dry-run of just the most recent archive. If there are no archives, skip
|
||||
the dry-run.
|
||||
'''
|
||||
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
|
||||
verbosity_flags = {
|
||||
VERBOSITY_SOME: ('--info',),
|
||||
VERBOSITY_LOTS: ('--debug',),
|
||||
}.get(verbosity, ())
|
||||
|
||||
full_command = (
|
||||
command, 'check',
|
||||
full_list_command = (
|
||||
command, 'list',
|
||||
'--short',
|
||||
repository,
|
||||
) + _make_check_flags(checks, check_last) + remote_path_flags + verbosity_flags
|
||||
) + remote_path_flags + verbosity_flags
|
||||
|
||||
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
|
||||
stdout = None if verbosity_flags else open(os.devnull, 'w')
|
||||
list_output = subprocess.check_output(full_list_command).decode(sys.stdout.encoding)
|
||||
|
||||
subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
|
||||
last_archive_name = list_output.strip().split('\n')[-1]
|
||||
if not last_archive_name:
|
||||
return
|
||||
|
||||
list_flag = ('--list',) if verbosity == VERBOSITY_LOTS else ()
|
||||
full_extract_command = (
|
||||
command, 'extract',
|
||||
'--dry-run',
|
||||
'{repository}::{last_archive_name}'.format(
|
||||
repository=repository,
|
||||
last_archive_name=last_archive_name,
|
||||
),
|
||||
) + remote_path_flags + verbosity_flags + list_flag
|
||||
|
||||
subprocess.check_call(full_extract_command)
|
||||
|
||||
@@ -5,12 +5,12 @@ from subprocess import CalledProcessError
|
||||
import sys
|
||||
|
||||
from borgmatic import borg
|
||||
from borgmatic.config import convert, validate
|
||||
from borgmatic.config import collect, convert, validate
|
||||
|
||||
|
||||
LEGACY_CONFIG_FILENAME = '/etc/borgmatic/config'
|
||||
DEFAULT_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
|
||||
DEFAULT_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
|
||||
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||
DEFAULT_CONFIG_PATHS = ['/etc/borgmatic/config.yaml', '/etc/borgmatic.d']
|
||||
DEFAULT_EXCLUDES_PATH = '/etc/borgmatic/excludes'
|
||||
|
||||
|
||||
def parse_arguments(*arguments):
|
||||
@@ -18,17 +18,43 @@ def parse_arguments(*arguments):
|
||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||
them as an ArgumentParser instance.
|
||||
'''
|
||||
parser = ArgumentParser()
|
||||
parser = ArgumentParser(
|
||||
description=
|
||||
'''
|
||||
A simple wrapper script for the Borg backup software that creates and prunes backups.
|
||||
If none of the --prune, --create, or --check options are given, then borgmatic defaults
|
||||
to all three: prune, create, and check archives.
|
||||
'''
|
||||
)
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
dest='config_filename',
|
||||
default=DEFAULT_CONFIG_FILENAME,
|
||||
help='Configuration filename',
|
||||
nargs='+',
|
||||
dest='config_paths',
|
||||
default=DEFAULT_CONFIG_PATHS,
|
||||
help='Configuration filenames or directories, defaults to: {}'.format(' '.join(DEFAULT_CONFIG_PATHS)),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--excludes',
|
||||
dest='excludes_filename',
|
||||
help='Excludes filename, deprecated in favor of exclude_patterns within configuration',
|
||||
help='Deprecated in favor of exclude_patterns within configuration',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-p', '--prune',
|
||||
dest='prune',
|
||||
action='store_true',
|
||||
help='Prune archives according to the retention policy',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-C', '--create',
|
||||
dest='create',
|
||||
action='store_true',
|
||||
help='Create archives (actually perform backups)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-k', '--check',
|
||||
dest='check',
|
||||
action='store_true',
|
||||
help='Check archives for consistency',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbosity',
|
||||
@@ -36,31 +62,50 @@ def parse_arguments(*arguments):
|
||||
help='Display verbose progress (1 for some, 2 for lots)',
|
||||
)
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
args = parser.parse_args(arguments)
|
||||
|
||||
# If any of the three action flags in the given parse arguments have been explicitly requested,
|
||||
# leave them as-is. Otherwise, assume defaults: Mutate the given arguments to enable all the
|
||||
# actions.
|
||||
if not args.prune and not args.create and not args.check:
|
||||
args.prune = True
|
||||
args.create = True
|
||||
args.check = True
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
try:
|
||||
args = parse_arguments(*sys.argv[1:])
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_FILENAME, args.config_filename)
|
||||
config = validate.parse_configuration(args.config_filename, validate.schema_filename())
|
||||
(location, storage, retention, consistency) = (
|
||||
config.get(section_name, {})
|
||||
for section_name in ('location', 'storage', 'retention', 'consistency')
|
||||
)
|
||||
remote_path = location.get('remote_path')
|
||||
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||
|
||||
borg.initialize(storage)
|
||||
if len(config_filenames) == 0:
|
||||
raise ValueError('Error: No configuration files found in: {}'.format(' '.join(args.config_paths)))
|
||||
|
||||
for repository in location['repositories']:
|
||||
borg.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
|
||||
borg.create_archive(
|
||||
args.verbosity,
|
||||
repository,
|
||||
location,
|
||||
storage,
|
||||
for config_filename in config_filenames:
|
||||
config = validate.parse_configuration(config_filename, validate.schema_filename())
|
||||
(location, storage, retention, consistency) = (
|
||||
config.get(section_name, {})
|
||||
for section_name in ('location', 'storage', 'retention', 'consistency')
|
||||
)
|
||||
borg.check_archives(args.verbosity, repository, consistency, remote_path=remote_path)
|
||||
remote_path = location.get('remote_path')
|
||||
|
||||
borg.initialize(storage)
|
||||
|
||||
for repository in location['repositories']:
|
||||
if args.prune:
|
||||
borg.prune_archives(args.verbosity, repository, retention, remote_path=remote_path)
|
||||
if args.create:
|
||||
borg.create_archive(
|
||||
args.verbosity,
|
||||
repository,
|
||||
location,
|
||||
storage,
|
||||
)
|
||||
if args.check:
|
||||
borg.check_archives(args.verbosity, repository, consistency, remote_path=remote_path)
|
||||
except (ValueError, OSError, CalledProcessError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
|
||||
def collect_config_filenames(config_paths):
|
||||
'''
|
||||
Given a sequence of config paths, both filenames and directories, resolve that to just an
|
||||
iterable of files. Accomplish this by listing any given directories looking for contained config
|
||||
files. This is non-recursive, so any directories within the given directories are ignored.
|
||||
|
||||
Return paths even if they don't exist on disk, so the user can find out about missing
|
||||
configuration paths. However, skip /etc/borgmatic.d if it's missing, so the user doesn't have to
|
||||
create it unless they need it.
|
||||
'''
|
||||
for path in config_paths:
|
||||
exists = os.path.exists(path)
|
||||
|
||||
if os.path.realpath(path) == '/etc/borgmatic.d' and not exists:
|
||||
continue
|
||||
|
||||
if not os.path.isdir(path) or not exists:
|
||||
yield path
|
||||
continue
|
||||
|
||||
for filename in os.listdir(path):
|
||||
full_filename = os.path.join(path, filename)
|
||||
if not os.path.isdir(full_filename):
|
||||
yield full_filename
|
||||
@@ -77,14 +77,19 @@ instead of the old one.'''
|
||||
)
|
||||
|
||||
|
||||
def guard_configuration_upgraded(source_config_filename, destination_config_filename):
|
||||
def guard_configuration_upgraded(source_config_filename, destination_config_filenames):
|
||||
'''
|
||||
If legacy souce configuration exists but destination upgraded config doesn't, raise
|
||||
If legacy source configuration exists but no destination upgraded configs do, raise
|
||||
LegacyConfigurationNotUpgraded.
|
||||
|
||||
The idea is that we want to alert the user about upgrading their config if they haven't already.
|
||||
'''
|
||||
if os.path.exists(source_config_filename) and not os.path.exists(destination_config_filename):
|
||||
destination_config_exists = any(
|
||||
os.path.exists(filename)
|
||||
for filename in destination_config_filenames
|
||||
)
|
||||
|
||||
if os.path.exists(source_config_filename) and not destination_config_exists:
|
||||
raise LegacyConfigurationNotUpgraded()
|
||||
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ map:
|
||||
Where to look for files to backup, and where to store those backups. See
|
||||
https://borgbackup.readthedocs.io/en/stable/quickstart.html and
|
||||
https://borgbackup.readthedocs.io/en/stable/usage.html#borg-create for details.
|
||||
required: True
|
||||
required: true
|
||||
map:
|
||||
source_directories:
|
||||
required: True
|
||||
required: true
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: List of source directories to backup (required). Globs are expanded.
|
||||
@@ -20,13 +20,13 @@ map:
|
||||
one_file_system:
|
||||
type: bool
|
||||
desc: Stay in same file system (do not cross mount points).
|
||||
example: yes
|
||||
example: true
|
||||
remote_path:
|
||||
type: scalar
|
||||
desc: Alternate Borg remote executable. Defaults to "borg".
|
||||
example: borg1
|
||||
repositories:
|
||||
required: True
|
||||
required: true
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: |
|
||||
@@ -106,21 +106,25 @@ map:
|
||||
consistency:
|
||||
desc: |
|
||||
Consistency checks to run after backups. See
|
||||
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check for details.
|
||||
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check and
|
||||
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-extract for details.
|
||||
map:
|
||||
checks:
|
||||
seq:
|
||||
- type: str
|
||||
enum: ['repository', 'archives', 'disabled']
|
||||
unique: True
|
||||
enum: ['repository', 'archives', 'extract', 'disabled']
|
||||
unique: true
|
||||
desc: |
|
||||
List of consistency checks to run: "repository", "archives", or both. Defaults
|
||||
to both. Set to "disabled" to disable all consistency checks. See
|
||||
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-check for details.
|
||||
List of one or more consistency checks to run: "repository", "archives", and/or
|
||||
"extract". Defaults to "repository" and "archives". Set to "disabled" to disable
|
||||
all consistency checks. "repository" checks the consistency of the repository,
|
||||
"archive" checks all of the archives, and "extract" does an extraction dry-run
|
||||
of just the most recent archive.
|
||||
example:
|
||||
- repository
|
||||
- archives
|
||||
check_last:
|
||||
type: int
|
||||
desc: Restrict the number of checked archives to the last n.
|
||||
desc: Restrict the number of checked archives to the last n. Applies only to the
|
||||
"archives" check.
|
||||
example: 3
|
||||
|
||||
@@ -9,27 +9,58 @@ from borgmatic.commands import borgmatic as module
|
||||
def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||
parser = module.parse_arguments()
|
||||
|
||||
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
|
||||
assert parser.config_paths == module.DEFAULT_CONFIG_PATHS
|
||||
assert parser.excludes_filename == None
|
||||
assert parser.verbosity is None
|
||||
|
||||
|
||||
def test_parse_arguments_with_filename_arguments_overrides_defaults():
|
||||
def test_parse_arguments_with_path_arguments_overrides_defaults():
|
||||
parser = module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
||||
|
||||
assert parser.config_filename == 'myconfig'
|
||||
assert parser.config_paths == ['myconfig']
|
||||
assert parser.excludes_filename == 'myexcludes'
|
||||
assert parser.verbosity is None
|
||||
|
||||
|
||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||
parser = module.parse_arguments('--config', 'myconfig', 'otherconfig')
|
||||
|
||||
assert parser.config_paths == ['myconfig', 'otherconfig']
|
||||
assert parser.verbosity is None
|
||||
|
||||
|
||||
def test_parse_arguments_with_verbosity_flag_overrides_default():
|
||||
parser = module.parse_arguments('--verbosity', '1')
|
||||
|
||||
assert parser.config_filename == module.DEFAULT_CONFIG_FILENAME
|
||||
assert parser.config_paths == module.DEFAULT_CONFIG_PATHS
|
||||
assert parser.excludes_filename == None
|
||||
assert parser.verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
||||
parser = module.parse_arguments()
|
||||
|
||||
assert parser.prune is True
|
||||
assert parser.create is True
|
||||
assert parser.check is True
|
||||
|
||||
|
||||
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
|
||||
parser = module.parse_arguments('--prune')
|
||||
|
||||
assert parser.prune is True
|
||||
assert parser.create is False
|
||||
assert parser.check is False
|
||||
|
||||
|
||||
def test_parse_arguments_with_multiple_actions_leaves_other_action_disabled():
|
||||
parser = module.parse_arguments('--create', '--check')
|
||||
|
||||
assert parser.prune is False
|
||||
assert parser.create is True
|
||||
assert parser.check is True
|
||||
|
||||
|
||||
def test_parse_arguments_with_invalid_arguments_exits():
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--posix-me-harder')
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import collect as module
|
||||
|
||||
|
||||
def test_collect_config_filenames_collects_given_files():
|
||||
config_paths = ('config.yaml', 'other.yaml')
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(False)
|
||||
|
||||
config_filenames = tuple(module.collect_config_filenames(config_paths))
|
||||
|
||||
assert config_filenames == config_paths
|
||||
|
||||
|
||||
def test_collect_config_filenames_collects_files_from_given_directories_and_ignores_sub_directories():
|
||||
config_paths = ('config.yaml', '/etc/borgmatic.d')
|
||||
mock_path = flexmock(module.os.path)
|
||||
mock_path.should_receive('exists').and_return(True)
|
||||
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d').and_return(True)
|
||||
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yaml').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/bar').and_return(True)
|
||||
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/baz.yaml').and_return(False)
|
||||
flexmock(module.os).should_receive('listdir').and_return(['foo.yaml', 'bar', 'baz.yaml'])
|
||||
|
||||
config_filenames = tuple(module.collect_config_filenames(config_paths))
|
||||
|
||||
assert config_filenames == (
|
||||
'config.yaml',
|
||||
'/etc/borgmatic.d/foo.yaml',
|
||||
'/etc/borgmatic.d/baz.yaml',
|
||||
)
|
||||
|
||||
|
||||
def test_collect_config_filenames_skips_etc_borgmatic_dot_d_if_it_does_not_exist():
|
||||
config_paths = ('config.yaml', '/etc/borgmatic.d')
|
||||
mock_path = flexmock(module.os.path)
|
||||
mock_path.should_receive('exists').with_args('config.yaml').and_return(True)
|
||||
mock_path.should_receive('exists').with_args('/etc/borgmatic.d').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d').and_return(True)
|
||||
|
||||
config_filenames = tuple(module.collect_config_filenames(config_paths))
|
||||
|
||||
assert config_filenames == ('config.yaml',)
|
||||
|
||||
|
||||
def test_collect_config_filenames_includes_directory_if_it_does_not_exist():
|
||||
config_paths = ('config.yaml', '/my/directory')
|
||||
mock_path = flexmock(module.os.path)
|
||||
mock_path.should_receive('exists').with_args('config.yaml').and_return(True)
|
||||
mock_path.should_receive('exists').with_args('/my/directory').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
|
||||
mock_path.should_receive('isdir').with_args('/my/directory').and_return(True)
|
||||
|
||||
config_filenames = tuple(module.collect_config_filenames(config_paths))
|
||||
|
||||
assert config_filenames == config_paths
|
||||
@@ -79,30 +79,34 @@ def test_convert_legacy_parsed_config_splits_space_separated_values():
|
||||
def test_guard_configuration_upgraded_raises_when_only_source_config_present():
|
||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(True)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
|
||||
|
||||
with pytest.raises(module.LegacyConfigurationNotUpgraded):
|
||||
module.guard_configuration_upgraded('config', 'config.yaml')
|
||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
||||
|
||||
|
||||
def test_guard_configuration_upgraded_does_not_raise_when_only_destination_config_present():
|
||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(True)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(True)
|
||||
|
||||
module.guard_configuration_upgraded('config', 'config.yaml')
|
||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
||||
|
||||
|
||||
def test_guard_configuration_upgraded_does_not_raise_when_both_configs_present():
|
||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(True)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(True)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(True)
|
||||
|
||||
module.guard_configuration_upgraded('config', 'config.yaml')
|
||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
||||
|
||||
|
||||
def test_guard_configuration_upgraded_does_not_raise_when_neither_config_present():
|
||||
flexmock(os.path).should_receive('exists').with_args('config').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
|
||||
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
|
||||
|
||||
module.guard_configuration_upgraded('config', 'config.yaml')
|
||||
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
|
||||
|
||||
|
||||
def test_guard_excludes_filename_omitted_raises_when_filename_provided():
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
import os
|
||||
|
||||
from flexmock import flexmock
|
||||
import pytest
|
||||
|
||||
from borgmatic import borg as module
|
||||
from borgmatic.verbosity import VERBOSITY_SOME, VERBOSITY_LOTS
|
||||
@@ -46,17 +47,23 @@ def test_write_exclude_file_with_empty_exclude_patterns_does_not_raise():
|
||||
|
||||
|
||||
def insert_subprocess_mock(check_call_command, **kwargs):
|
||||
subprocess = flexmock(STDOUT=STDOUT)
|
||||
subprocess = flexmock(module.subprocess)
|
||||
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
|
||||
flexmock(module).subprocess = subprocess
|
||||
|
||||
|
||||
def insert_subprocess_never():
|
||||
subprocess = flexmock()
|
||||
subprocess = flexmock(module.subprocess)
|
||||
subprocess.should_receive('check_call').never()
|
||||
flexmock(module).subprocess = subprocess
|
||||
|
||||
|
||||
def insert_subprocess_check_output_mock(check_output_command, result, **kwargs):
|
||||
subprocess = flexmock(module.subprocess)
|
||||
subprocess.should_receive('check_output').with_args(check_output_command, **kwargs).and_return(result).once()
|
||||
flexmock(module).subprocess = subprocess
|
||||
|
||||
|
||||
def insert_platform_mock():
|
||||
flexmock(module.platform).should_receive('node').and_return('host')
|
||||
|
||||
@@ -395,9 +402,15 @@ def test_parse_checks_with_disabled_returns_no_checks():
|
||||
|
||||
|
||||
def test_make_check_flags_with_checks_returns_flags():
|
||||
flags = module._make_check_flags(('foo', 'bar'))
|
||||
flags = module._make_check_flags(('repository',))
|
||||
|
||||
assert flags == ('--foo-only', '--bar-only')
|
||||
assert flags == ('--repository-only',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_extract_check_does_not_make_extract_flag():
|
||||
flags = module._make_check_flags(('extract',))
|
||||
|
||||
assert flags == ()
|
||||
|
||||
|
||||
def test_make_check_flags_with_default_checks_returns_no_flags():
|
||||
@@ -407,19 +420,27 @@ def test_make_check_flags_with_default_checks_returns_no_flags():
|
||||
|
||||
|
||||
def test_make_check_flags_with_checks_and_last_returns_flags_including_last():
|
||||
flags = module._make_check_flags(('foo', 'bar'), check_last=3)
|
||||
flags = module._make_check_flags(('repository',), check_last=3)
|
||||
|
||||
assert flags == ('--foo-only', '--bar-only', '--last', '3')
|
||||
assert flags == ('--repository-only', '--last', '3')
|
||||
|
||||
|
||||
def test_make_check_flags_with_last_returns_last_flag():
|
||||
def test_make_check_flags_with_default_checks_and_last_returns_last_flag():
|
||||
flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
|
||||
|
||||
assert flags == ('--last', '3')
|
||||
|
||||
|
||||
def test_check_archives_should_call_borg_with_parameters():
|
||||
checks = flexmock()
|
||||
@pytest.mark.parametrize(
|
||||
'checks',
|
||||
(
|
||||
('repository',),
|
||||
('archives',),
|
||||
('repository', 'archives'),
|
||||
('repository', 'archives', 'other'),
|
||||
),
|
||||
)
|
||||
def test_check_archives_should_call_borg_with_parameters(checks):
|
||||
check_last = flexmock()
|
||||
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
@@ -442,9 +463,27 @@ def test_check_archives_should_call_borg_with_parameters():
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_extract_check_should_call_extract_only():
|
||||
checks = ('extract',)
|
||||
check_last = flexmock()
|
||||
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').never()
|
||||
flexmock(module).should_receive('extract_last_archive_dry_run').once()
|
||||
insert_subprocess_never()
|
||||
|
||||
module.check_archives(
|
||||
verbosity=None,
|
||||
repository='repo',
|
||||
consistency_config=consistency_config,
|
||||
command='borg',
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter():
|
||||
checks = ('repository',)
|
||||
consistency_config = flexmock().should_receive('get').and_return(None).mock
|
||||
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').and_return(())
|
||||
insert_subprocess_mock(
|
||||
('borg', 'check', 'repo', '--info'),
|
||||
@@ -462,8 +501,9 @@ def test_check_archives_with_verbosity_some_should_call_borg_with_info_parameter
|
||||
|
||||
|
||||
def test_check_archives_with_verbosity_lots_should_call_borg_with_debug_parameter():
|
||||
checks = ('repository',)
|
||||
consistency_config = flexmock().should_receive('get').and_return(None).mock
|
||||
flexmock(module).should_receive('_parse_checks').and_return(flexmock())
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').and_return(())
|
||||
insert_subprocess_mock(
|
||||
('borg', 'check', 'repo', '--debug'),
|
||||
@@ -494,7 +534,7 @@ def test_check_archives_without_any_checks_should_bail():
|
||||
|
||||
|
||||
def test_check_archives_with_remote_path_should_call_borg_with_remote_path_parameters():
|
||||
checks = flexmock()
|
||||
checks = ('repository',)
|
||||
check_last = flexmock()
|
||||
consistency_config = flexmock().should_receive('get').and_return(check_last).mock
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
@@ -516,3 +556,87 @@ def test_check_archives_with_remote_path_should_call_borg_with_remote_path_param
|
||||
command='borg',
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_last_archive_dry_run_should_call_borg_with_last_archive():
|
||||
flexmock(sys.stdout).encoding = 'utf-8'
|
||||
insert_subprocess_check_output_mock(
|
||||
('borg', 'list', '--short', 'repo'),
|
||||
result='archive1\narchive2\n'.encode('utf-8'),
|
||||
)
|
||||
insert_subprocess_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2'),
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(
|
||||
verbosity=None,
|
||||
repository='repo',
|
||||
command='borg',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_last_archive_dry_run_without_any_archives_should_bail():
|
||||
flexmock(sys.stdout).encoding = 'utf-8'
|
||||
insert_subprocess_check_output_mock(
|
||||
('borg', 'list', '--short', 'repo'),
|
||||
result='\n'.encode('utf-8'),
|
||||
)
|
||||
insert_subprocess_never()
|
||||
|
||||
module.extract_last_archive_dry_run(
|
||||
verbosity=None,
|
||||
repository='repo',
|
||||
command='borg',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_last_archive_dry_run_with_verbosity_some_should_call_borg_with_info_parameter():
|
||||
flexmock(sys.stdout).encoding = 'utf-8'
|
||||
insert_subprocess_check_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--info'),
|
||||
result='archive1\narchive2\n'.encode('utf-8'),
|
||||
)
|
||||
insert_subprocess_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--info'),
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(
|
||||
verbosity=VERBOSITY_SOME,
|
||||
repository='repo',
|
||||
command='borg',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_last_archive_dry_run_with_verbosity_lots_should_call_borg_with_debug_parameter():
|
||||
flexmock(sys.stdout).encoding = 'utf-8'
|
||||
insert_subprocess_check_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--debug'),
|
||||
result='archive1\narchive2\n'.encode('utf-8'),
|
||||
)
|
||||
insert_subprocess_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--debug', '--list'),
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(
|
||||
verbosity=VERBOSITY_LOTS,
|
||||
repository='repo',
|
||||
command='borg',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_last_archive_dry_run_should_call_borg_with_remote_path_parameters():
|
||||
flexmock(sys.stdout).encoding = 'utf-8'
|
||||
insert_subprocess_check_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'),
|
||||
result='archive1\narchive2\n'.encode('utf-8'),
|
||||
)
|
||||
insert_subprocess_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--remote-path', 'borg1'),
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(
|
||||
verbosity=None,
|
||||
repository='repo',
|
||||
command='borg',
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user