mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7490b56d1 | ||
|
|
66eb18d5ea | ||
|
|
46486138b6 | ||
|
|
d6562c4b1e | ||
|
|
1ddde0910c | ||
|
|
79f3b84ca2 | ||
|
|
55141bda67 | ||
|
|
bc02c123e6 | ||
|
|
e76d5ad988 | ||
|
|
8ad8a9c422 | ||
|
|
b15c9b7dab | ||
|
|
2405e97c38 | ||
|
|
fdbb2ee905 | ||
|
|
94b9ef56be | ||
|
|
952168ce25 | ||
|
|
5273037a94 | ||
|
|
53e6ff9524 | ||
|
|
f66fd1caaa | ||
|
|
d93fdbc5ad | ||
|
|
58e0439daf | ||
|
|
75b5e7254e | ||
|
|
39550a7fe9 | ||
|
|
5f0c084bee | ||
|
|
88f06f7921 | ||
|
|
8d12079386 | ||
|
|
7824a034ca | ||
|
|
8ef0ba2fae | ||
|
|
cc384f4324 | ||
|
|
8a91c79fb0 | ||
|
|
ac1d63bb0d | ||
|
|
83632448be | ||
|
|
e108526bab | ||
|
|
e27ba0d08a | ||
|
|
5afe0e3d63 | ||
|
|
c52f82f9ce | ||
|
|
d0c533555e | ||
|
|
1995c80e60 | ||
|
|
24e1516ec5 | ||
|
|
5b1beda82b |
@@ -1,3 +1,33 @@
|
||||
1.5.1
|
||||
* #289: Tired of looking up the latest successful archive name in order to pass it to borgmatic
|
||||
actions? Me too. Now you can specify "--archive latest" to all actions that accept an archive
|
||||
flag.
|
||||
* #290: Fix the "--stats" and "--files" flags so that they yield output at verbosity 0.
|
||||
* Reduce the default verbosity of borgmatic logs sent to Healthchecks monitoring hook. Now, it's
|
||||
warnings and errors only. You can increase the verbosity via the "--monitoring-verbosity" flag.
|
||||
* Add security policy documentation in SECURITY.md.
|
||||
|
||||
1.5.0
|
||||
* #245: Monitor backups with PagerDuty hook integration. See the documentation for more
|
||||
information: https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
|
||||
* #255: Add per-action hooks: "before_prune", "after_prune", "before_check", and "after_check".
|
||||
* #274: Add ~/.config/borgmatic.d as another configuration directory default.
|
||||
* #277: Customize Healthchecks log level via borgmatic "--monitoring-verbosity" flag.
|
||||
* #280: Change "exclude_if_present" option to support multiple filenames that indicate a directory
|
||||
should be excluded from backups, rather than just a single filename.
|
||||
* #284: Backup to a removable drive or intermittent server via "soft failure" feature. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
|
||||
* #287: View consistency check progress via "--progress" flag for "check" action.
|
||||
* For "create" and "prune" actions, no longer list files or show detailed stats at any verbosities
|
||||
by default. You can opt back in with "--files" or "--stats" flags.
|
||||
* For "list" and "info" actions, show repository names even at verbosity 0.
|
||||
|
||||
1.4.22
|
||||
* #276, #285: Disable colored output when "--json" flag is used, so as to produce valid JSON ouput.
|
||||
* After a backup of a database dump in directory format, properly remove the dump directory.
|
||||
* In "borgmatic --help", don't expand $HOME in listing of default "--config" paths.
|
||||
|
||||
1.4.21
|
||||
* #268: Override particular configuration options from the command-line via "--override" flag. See
|
||||
the documentation for more information:
|
||||
|
||||
@@ -20,9 +20,11 @@ location:
|
||||
- /home
|
||||
- /etc
|
||||
|
||||
# Paths to local or remote repositories.
|
||||
# Paths of local or remote repositories to backup to.
|
||||
repositories:
|
||||
- user@backupserver:sourcehostname.borg
|
||||
- 1234@usw-s001.rsync.net:backups.borg
|
||||
- k8pDxu32@k8pDxu32.repo.borgbase.com:repo
|
||||
- /var/lib/backups/local.borg
|
||||
|
||||
retention:
|
||||
# Retention policy for how many backups to keep.
|
||||
@@ -64,6 +66,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.rsync.net/cgi-bin/borg.cgi?campaign=borg&adgroup=borgmatic"><img src="docs/static/rsyncnet.png" alt="rsync.net" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px;"></a>
|
||||
|
||||
@@ -78,6 +81,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
|
||||
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [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/)
|
||||
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
||||
@@ -115,7 +119,12 @@ If you'd like to chat with borgmatic developers or users, head on over to the
|
||||
href="https://webchat.freenode.net/?channels=borgmatic">web chat</a> or a
|
||||
native <a href="irc://chat.freenode.net:6697">IRC client</a>.
|
||||
|
||||
Other questions or comments? Contact <mailto:witten@torsion.org>.
|
||||
Also see the [security
|
||||
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
|
||||
issues.
|
||||
|
||||
Other questions or comments? Contact
|
||||
[witten@torsion.org](mailto:witten@torsion.org).
|
||||
|
||||
|
||||
### Contributing
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Security policy
|
||||
permalink: security-policy/index.html
|
||||
---
|
||||
|
||||
## Supported versions
|
||||
|
||||
While we want to hear about security vulnerabilities in all versions of
|
||||
borgmatic, security fixes will only be made to the most recently released
|
||||
version. It's not practical for our small volunteer effort to maintain
|
||||
multiple different release branches and put out separate security patches for
|
||||
each.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you find a security vulnerability, please [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) or [send email
|
||||
directly](mailto:witten@torsion.org) as appropriate. You should expect to hear
|
||||
back within a few days at most, and generally sooner.
|
||||
@@ -91,13 +91,15 @@ def check_archives(
|
||||
consistency_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
progress=None,
|
||||
repair=None,
|
||||
only_checks=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a storage config dict, a consistency config dict,
|
||||
local/remote commands to run, whether to attempt a repair, and an optional list of checks
|
||||
to use instead of configured checks, check the contained Borg archives for consistency.
|
||||
local/remote commands to run, whether to include progress information, whether to attempt a
|
||||
repair, and an optional list of checks to use instead of configured checks, check the contained
|
||||
Borg archives for consistency.
|
||||
|
||||
If there are no consistency checks to run, skip running them.
|
||||
'''
|
||||
@@ -124,17 +126,17 @@ def check_archives(
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ verbosity_flags
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
# The Borg repair option trigger an interactive prompt, which won't work when output is
|
||||
# captured.
|
||||
if repair:
|
||||
# captured. And progress messes with the terminal directly.
|
||||
if repair or progress:
|
||||
execute_command_without_capture(full_command, error_on_warnings=True)
|
||||
return
|
||||
|
||||
execute_command(full_command, error_on_warnings=True)
|
||||
else:
|
||||
execute_command(full_command, error_on_warnings=True)
|
||||
|
||||
if 'extract' in checks:
|
||||
extract.extract_last_archive_dry_run(repository, lock_wait, local_path, remote_path)
|
||||
|
||||
+10
-13
@@ -88,8 +88,12 @@ def _make_exclude_flags(location_config, exclude_filename=None):
|
||||
)
|
||||
)
|
||||
caches_flag = ('--exclude-caches',) if location_config.get('exclude_caches') else ()
|
||||
if_present = location_config.get('exclude_if_present')
|
||||
if_present_flags = ('--exclude-if-present', if_present) if if_present else ()
|
||||
if_present_flags = tuple(
|
||||
itertools.chain.from_iterable(
|
||||
('--exclude-if-present', if_present)
|
||||
for if_present in location_config.get('exclude_if_present', ())
|
||||
)
|
||||
)
|
||||
keep_exclude_tags_flags = (
|
||||
('--keep-exclude-tags',) if location_config.get('keep_exclude_tags') else ()
|
||||
)
|
||||
@@ -131,6 +135,7 @@ def create_archive(
|
||||
progress=False,
|
||||
stats=False,
|
||||
json=False,
|
||||
files=False,
|
||||
):
|
||||
'''
|
||||
Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
|
||||
@@ -175,17 +180,9 @@ def create_archive(
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (
|
||||
('--list', '--filter', 'AME-')
|
||||
if logger.isEnabledFor(logging.INFO) and not json and not progress
|
||||
else ()
|
||||
)
|
||||
+ (('--list', '--filter', 'AME-') if files and not json and not progress else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
||||
+ (
|
||||
('--stats',)
|
||||
if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) and not json
|
||||
else ()
|
||||
)
|
||||
+ (('--stats',) if stats and not json and not dry_run else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
@@ -207,7 +204,7 @@ def create_archive(
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif stats:
|
||||
elif (stats or files) and logger.getEffectiveLevel() == logging.WARNING:
|
||||
output_log_level = logging.WARNING
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
|
||||
@@ -11,6 +11,42 @@ logger = logging.getLogger(__name__)
|
||||
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
|
||||
|
||||
|
||||
def resolve_archive_name(repository, archive, storage_config, local_path='borg', remote_path=None):
|
||||
'''
|
||||
Given a local or remote repository path, an archive name, a storage config dict, a local Borg
|
||||
path, and a remote Borg path, simply return the archive name. But if the archive name is
|
||||
"latest", then instead introspect the repository for the latest successful (non-checkpoint)
|
||||
archive, and return its name.
|
||||
|
||||
Raise ValueError if "latest" is given but there are no archives in the repository.
|
||||
'''
|
||||
if archive != "latest":
|
||||
return archive
|
||||
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
(local_path, 'list')
|
||||
+ (('--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)
|
||||
+ make_flags('glob-archives', BORG_EXCLUDE_CHECKPOINTS_GLOB)
|
||||
+ make_flags('last', 1)
|
||||
+ ('--short', repository)
|
||||
)
|
||||
|
||||
output = execute_command(full_command, output_log_level=None, error_on_warnings=False)
|
||||
try:
|
||||
latest_archive = output.strip().splitlines()[-1]
|
||||
except IndexError:
|
||||
raise ValueError('No archives found in the repository')
|
||||
|
||||
logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
|
||||
|
||||
return latest_archive
|
||||
|
||||
|
||||
def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
|
||||
'''
|
||||
Given a local or remote repository path, a storage config dict, and the arguments to the list
|
||||
|
||||
+11
-9
@@ -41,6 +41,7 @@ def prune_archives(
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
stats=False,
|
||||
files=False,
|
||||
):
|
||||
'''
|
||||
Given dry-run flag, a local or remote repository path, a storage config dict, and a
|
||||
@@ -57,17 +58,18 @@ def prune_archives(
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
|
||||
+ (('--info', '--list') if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--stats',) if stats and not dry_run else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--list',) if files else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--stats',) if stats else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(
|
||||
full_command,
|
||||
output_log_level=logging.WARNING if stats else logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
if (stats or files) and logger.getEffectiveLevel() == logging.WARNING:
|
||||
output_log_level = logging.WARNING
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
|
||||
execute_command(full_command, output_log_level=output_log_level, error_on_warnings=False)
|
||||
|
||||
@@ -106,7 +106,8 @@ def parse_arguments(*unparsed_arguments):
|
||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||
them as a dict mapping from subparser name (or "global") to an argparse.Namespace instance.
|
||||
'''
|
||||
config_paths = collect.get_default_config_paths()
|
||||
config_paths = collect.get_default_config_paths(expand_home=True)
|
||||
unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
|
||||
|
||||
global_parser = ArgumentParser(add_help=False)
|
||||
global_group = global_parser.add_argument_group('global arguments')
|
||||
@@ -118,7 +119,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
dest='config_paths',
|
||||
default=config_paths,
|
||||
help='Configuration filenames or directories, defaults to: {}'.format(
|
||||
' '.join(config_paths)
|
||||
' '.join(unexpanded_config_paths)
|
||||
),
|
||||
)
|
||||
global_group.add_argument(
|
||||
@@ -158,6 +159,13 @@ def parse_arguments(*unparsed_arguments):
|
||||
default=0,
|
||||
help='Log verbose progress to log file (from only errors to very verbose: -1, 0, 1, or 2). Only used when --log-file is given',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--monitoring-verbosity',
|
||||
type=int,
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to monitoring integrations that support logging (from only errors to very verbose: -1, 0, 1, or 2)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file',
|
||||
type=str,
|
||||
@@ -236,6 +244,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
action='store_true',
|
||||
help='Display statistics of archive',
|
||||
)
|
||||
prune_group.add_argument(
|
||||
'--files', dest='files', default=False, action='store_true', help='Show per-file details'
|
||||
)
|
||||
prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
create_parser = subparsers.add_parser(
|
||||
@@ -251,7 +262,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
dest='progress',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display progress for each file as it is processed',
|
||||
help='Display progress for each file as it is backed up',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--stats',
|
||||
@@ -260,6 +271,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
action='store_true',
|
||||
help='Display statistics of archive',
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--files', dest='files', default=False, action='store_true', help='Show per-file details'
|
||||
)
|
||||
create_group.add_argument(
|
||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||
)
|
||||
@@ -273,6 +287,13 @@ def parse_arguments(*unparsed_arguments):
|
||||
add_help=False,
|
||||
)
|
||||
check_group = check_parser.add_argument_group('check arguments')
|
||||
check_group.add_argument(
|
||||
'--progress',
|
||||
dest='progress',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display progress for each file as it is checked',
|
||||
)
|
||||
check_group.add_argument(
|
||||
'--repair',
|
||||
dest='repair',
|
||||
@@ -302,7 +323,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--repository',
|
||||
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
||||
)
|
||||
extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
|
||||
extract_group.add_argument(
|
||||
'--archive', help='Name of archive to extract (or "latest")', required=True
|
||||
)
|
||||
extract_group.add_argument(
|
||||
'--path',
|
||||
'--restore-path',
|
||||
@@ -322,7 +345,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
dest='progress',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display progress for each file as it is processed',
|
||||
help='Display progress for each file as it is extracted',
|
||||
)
|
||||
extract_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
@@ -340,7 +363,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--repository',
|
||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
||||
)
|
||||
mount_group.add_argument('--archive', help='Name of archive to mount')
|
||||
mount_group.add_argument('--archive', help='Name of archive to mount (or "latest")')
|
||||
mount_group.add_argument(
|
||||
'--mount-point',
|
||||
metavar='PATH',
|
||||
@@ -394,7 +417,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--repository',
|
||||
help='Path of repository to restore from, defaults to the configured repository if there is only one',
|
||||
)
|
||||
restore_group.add_argument('--archive', help='Name of archive to restore from', required=True)
|
||||
restore_group.add_argument(
|
||||
'--archive', help='Name of archive to restore from (or "latest")', required=True
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--database',
|
||||
metavar='NAME',
|
||||
@@ -425,7 +450,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--repository',
|
||||
help='Path of repository to list, defaults to the configured repository if there is only one',
|
||||
)
|
||||
list_group.add_argument('--archive', help='Name of archive to list')
|
||||
list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
|
||||
list_group.add_argument(
|
||||
'--path',
|
||||
metavar='PATH',
|
||||
@@ -487,7 +512,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--repository',
|
||||
help='Path of repository to show info for, defaults to the configured repository if there is only one',
|
||||
)
|
||||
info_group.add_argument('--archive', help='Name of archive to show info for')
|
||||
info_group.add_argument('--archive', help='Name of archive to show info for (or "latest")')
|
||||
info_group.add_argument(
|
||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -53,6 +54,7 @@ def run_configuration(config_filename, config, arguments):
|
||||
encountered_error = None
|
||||
error_repository = ''
|
||||
prune_create_or_check = {'prune', 'create', 'check'}.intersection(arguments)
|
||||
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
||||
|
||||
try:
|
||||
if prune_create_or_check:
|
||||
@@ -62,6 +64,15 @@ def run_configuration(config_filename, config, arguments):
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.START,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if 'prune' in arguments:
|
||||
command.execute_hook(
|
||||
hooks.get('before_prune'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'pre-prune',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if 'create' in arguments:
|
||||
@@ -80,10 +91,21 @@ def run_configuration(config_filename, config, arguments):
|
||||
location,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if 'check' in arguments:
|
||||
command.execute_hook(
|
||||
hooks.get('before_check'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'pre-check',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(config_filename, error):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running pre-backup hook'.format(config_filename), error
|
||||
'{}: Error running pre hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
if not encountered_error:
|
||||
@@ -109,6 +131,14 @@ def run_configuration(config_filename, config, arguments):
|
||||
|
||||
if not encountered_error:
|
||||
try:
|
||||
if 'prune' in arguments:
|
||||
command.execute_hook(
|
||||
hooks.get('after_prune'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'post-prune',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if 'create' in arguments:
|
||||
dispatch.call_hooks(
|
||||
'remove_database_dumps',
|
||||
@@ -125,6 +155,14 @@ def run_configuration(config_filename, config, arguments):
|
||||
'post-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if 'check' in arguments:
|
||||
command.execute_hook(
|
||||
hooks.get('after_check'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'post-check',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
if {'prune', 'create', 'check'}.intersection(arguments):
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
@@ -132,12 +170,16 @@ def run_configuration(config_filename, config, arguments):
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.FINISH,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(config_filename, error):
|
||||
return
|
||||
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running post-backup hook'.format(config_filename), error
|
||||
'{}: Error running post hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
if encountered_error and prune_create_or_check:
|
||||
@@ -158,9 +200,13 @@ def run_configuration(config_filename, config, arguments):
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.FAIL,
|
||||
monitoring_log_level,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
if command.considered_soft_failure(config_filename, error):
|
||||
return
|
||||
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running on-error hook'.format(config_filename), error
|
||||
)
|
||||
@@ -212,6 +258,7 @@ def run_actions(
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
stats=arguments['prune'].stats,
|
||||
files=arguments['prune'].files,
|
||||
)
|
||||
if 'create' in arguments:
|
||||
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
||||
@@ -225,6 +272,7 @@ def run_actions(
|
||||
progress=arguments['create'].progress,
|
||||
stats=arguments['create'].stats,
|
||||
json=arguments['create'].json,
|
||||
files=arguments['create'].files,
|
||||
)
|
||||
if json_output:
|
||||
yield json.loads(json_output)
|
||||
@@ -236,6 +284,7 @@ def run_actions(
|
||||
consistency,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
progress=arguments['check'].progress,
|
||||
repair=arguments['check'].repair,
|
||||
only_checks=arguments['check'].only,
|
||||
)
|
||||
@@ -249,7 +298,9 @@ def run_actions(
|
||||
borg_extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
arguments['extract'].archive,
|
||||
borg_list.resolve_archive_name(
|
||||
repository, arguments['extract'].archive, storage, local_path, remote_path
|
||||
),
|
||||
arguments['extract'].paths,
|
||||
location,
|
||||
storage,
|
||||
@@ -271,7 +322,9 @@ def run_actions(
|
||||
|
||||
borg_mount.mount_archive(
|
||||
repository,
|
||||
arguments['mount'].archive,
|
||||
borg_list.resolve_archive_name(
|
||||
repository, arguments['mount'].archive, storage, local_path, remote_path
|
||||
),
|
||||
arguments['mount'].mount_point,
|
||||
arguments['mount'].paths,
|
||||
arguments['mount'].foreground,
|
||||
@@ -307,7 +360,9 @@ def run_actions(
|
||||
borg_extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
arguments['restore'].archive,
|
||||
borg_list.resolve_archive_name(
|
||||
repository, arguments['restore'].archive, storage, local_path, remote_path
|
||||
),
|
||||
dump.convert_glob_patterns_to_borg_patterns(
|
||||
dump.flatten_dump_patterns(dump_patterns, restore_names)
|
||||
),
|
||||
@@ -347,11 +402,16 @@ def run_actions(
|
||||
if arguments['list'].repository is None or validate.repositories_match(
|
||||
repository, arguments['list'].repository
|
||||
):
|
||||
logger.info('{}: Listing archives'.format(repository))
|
||||
list_arguments = copy.copy(arguments['list'])
|
||||
if not list_arguments.json:
|
||||
logger.warning('{}: Listing archives'.format(repository))
|
||||
list_arguments.archive = borg_list.resolve_archive_name(
|
||||
repository, list_arguments.archive, storage, local_path, remote_path
|
||||
)
|
||||
json_output = borg_list.list_archives(
|
||||
repository,
|
||||
storage,
|
||||
list_arguments=arguments['list'],
|
||||
list_arguments=list_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
@@ -361,11 +421,16 @@ def run_actions(
|
||||
if arguments['info'].repository is None or validate.repositories_match(
|
||||
repository, arguments['info'].repository
|
||||
):
|
||||
logger.info('{}: Displaying summary info for archives'.format(repository))
|
||||
info_arguments = copy.copy(arguments['info'])
|
||||
if not info_arguments.json:
|
||||
logger.warning('{}: Displaying summary info for archives'.format(repository))
|
||||
info_arguments.archive = borg_list.resolve_archive_name(
|
||||
repository, info_arguments.archive, storage, local_path, remote_path
|
||||
)
|
||||
json_output = borg_info.display_archives_info(
|
||||
repository,
|
||||
storage,
|
||||
info_arguments=arguments['info'],
|
||||
info_arguments=info_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
@@ -587,12 +652,19 @@ def main(): # pragma: no cover
|
||||
config_filenames = tuple(collect.collect_config_filenames(global_arguments.config_paths))
|
||||
configs, parse_logs = load_configurations(config_filenames, global_arguments.overrides)
|
||||
|
||||
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
|
||||
any_json_flags = any(
|
||||
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
|
||||
)
|
||||
colorama.init(
|
||||
autoreset=True,
|
||||
strip=not should_do_markup(global_arguments.no_color or any_json_flags, configs),
|
||||
)
|
||||
try:
|
||||
configure_logging(
|
||||
verbosity_to_log_level(global_arguments.verbosity),
|
||||
verbosity_to_log_level(global_arguments.syslog_verbosity),
|
||||
verbosity_to_log_level(global_arguments.log_file_verbosity),
|
||||
verbosity_to_log_level(global_arguments.monitoring_verbosity),
|
||||
global_arguments.log_file,
|
||||
)
|
||||
except (FileNotFoundError, PermissionError) as error:
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_default_config_paths():
|
||||
def get_default_config_paths(expand_home=True):
|
||||
'''
|
||||
Based on the value of the XDG_CONFIG_HOME and HOME environment variables, return a list of
|
||||
default configuration paths. This includes both system-wide configuration and configuration in
|
||||
the current user's home directory.
|
||||
|
||||
Don't expand the home directory ($HOME) if the expand home flag is False.
|
||||
'''
|
||||
user_config_directory = os.getenv('XDG_CONFIG_HOME') or os.path.expandvars(
|
||||
os.path.join('$HOME', '.config')
|
||||
)
|
||||
user_config_directory = os.getenv('XDG_CONFIG_HOME') or os.path.join('$HOME', '.config')
|
||||
if expand_home:
|
||||
user_config_directory = os.path.expandvars(user_config_directory)
|
||||
|
||||
return [
|
||||
'/etc/borgmatic/config.yaml',
|
||||
'/etc/borgmatic.d',
|
||||
'%s/borgmatic/config.yaml' % user_config_directory,
|
||||
'%s/borgmatic.d' % user_config_directory,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
def normalize(config):
|
||||
'''
|
||||
Given a configuration dict, apply particular hard-coded rules to normalize its contents to
|
||||
adhere to the configuration schema.
|
||||
'''
|
||||
exclude_if_present = config.get('location', {}).get('exclude_if_present')
|
||||
|
||||
# "Upgrade" exclude_if_present from a string to a list.
|
||||
if isinstance(exclude_if_present, str):
|
||||
config['location']['exclude_if_present'] = [exclude_if_present]
|
||||
@@ -121,11 +121,13 @@ map:
|
||||
http://www.brynosaurus.com/cachedir/spec.html for details. Defaults to false.
|
||||
example: true
|
||||
exclude_if_present:
|
||||
type: str
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
Exclude directories that contain a file with the given filename. Defaults to not
|
||||
Exclude directories that contain a file with the given filenames. Defaults to not
|
||||
set.
|
||||
example: .nobackup
|
||||
example:
|
||||
- .nobackup
|
||||
keep_exclude_tags:
|
||||
type: bool
|
||||
desc: |
|
||||
@@ -393,6 +395,22 @@ map:
|
||||
backup, run once per configuration file.
|
||||
example:
|
||||
- echo "Starting a backup."
|
||||
before_prune:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute before pruning, run
|
||||
once per configuration file.
|
||||
example:
|
||||
- echo "Starting pruning."
|
||||
before_check:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute before consistency
|
||||
checks, run once per configuration file.
|
||||
example:
|
||||
- echo "Starting checks."
|
||||
after_backup:
|
||||
seq:
|
||||
- type: str
|
||||
@@ -400,15 +418,32 @@ map:
|
||||
List of one or more shell commands or scripts to execute after creating a
|
||||
backup, run once per configuration file.
|
||||
example:
|
||||
- echo "Created a backup."
|
||||
- echo "Finished a backup."
|
||||
after_prune:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute after pruning, run once
|
||||
per configuration file.
|
||||
example:
|
||||
- echo "Finished pruning."
|
||||
after_check:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute after consistency
|
||||
checks, run once per configuration file.
|
||||
example:
|
||||
- echo "Finished checks."
|
||||
on_error:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute when an exception
|
||||
occurs during a backup or when running a before_backup or after_backup hook.
|
||||
occurs during a "prune", "create", or "check" action or an associated
|
||||
before/after hook.
|
||||
example:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
- echo "Error during prune/create/check."
|
||||
postgresql_databases:
|
||||
seq:
|
||||
- map:
|
||||
@@ -532,6 +567,15 @@ map:
|
||||
for details.
|
||||
example:
|
||||
https://cronitor.link/d3x0c1
|
||||
pagerduty:
|
||||
type: str
|
||||
desc: |
|
||||
PagerDuty integration key used to notify PagerDuty when a backup errors. Create
|
||||
an account at https://www.pagerduty.com/ if you'd like to use this service. See
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook
|
||||
for details.
|
||||
example:
|
||||
a177cad45bd374409f78906a810a3074
|
||||
cronhub:
|
||||
type: str
|
||||
desc: |
|
||||
@@ -546,7 +590,8 @@ map:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute before running all
|
||||
actions (if one of them is "create"), run once before all configuration files.
|
||||
actions (if one of them is "create"). These are collected from all configuration
|
||||
files and then run once before all of them (prior to all actions).
|
||||
example:
|
||||
- echo "Starting actions."
|
||||
after_everything:
|
||||
@@ -554,7 +599,8 @@ map:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute after running all
|
||||
actions (if one of them is "create"), run once after all configuration files.
|
||||
actions (if one of them is "create"). These are collected from all configuration
|
||||
files and then run once before all of them (prior to all actions).
|
||||
example:
|
||||
- echo "Completed actions."
|
||||
umask:
|
||||
|
||||
@@ -6,7 +6,7 @@ import pykwalify.core
|
||||
import pykwalify.errors
|
||||
import ruamel.yaml
|
||||
|
||||
from borgmatic.config import load, override
|
||||
from borgmatic.config import load, normalize, override
|
||||
|
||||
|
||||
def schema_filename():
|
||||
@@ -104,6 +104,7 @@ def parse_configuration(config_filename, schema_filename, overrides=None):
|
||||
raise Validation_error(config_filename, (str(error),))
|
||||
|
||||
override.apply_overrides(config, overrides)
|
||||
normalize.normalize(config)
|
||||
|
||||
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
|
||||
parsed_result = validator.validate(raise_exception=False)
|
||||
|
||||
@@ -6,6 +6,9 @@ from borgmatic import execute
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SOFT_FAIL_EXIT_CODE = 75
|
||||
|
||||
|
||||
def interpolate_context(command, context):
|
||||
'''
|
||||
Given a single hook command and a dict of context names/values, interpolate the values by
|
||||
@@ -69,3 +72,24 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
|
||||
finally:
|
||||
if original_umask:
|
||||
os.umask(original_umask)
|
||||
|
||||
|
||||
def considered_soft_failure(config_filename, error):
|
||||
'''
|
||||
Given a configuration filename and an exception object, return whether the exception object
|
||||
represents a subprocess.CalledProcessError with a return code of SOFT_FAIL_EXIT_CODE. If so,
|
||||
that indicates that the error is a "soft failure", and should not result in an error.
|
||||
'''
|
||||
exit_code = getattr(error, 'returncode', None)
|
||||
if exit_code is None:
|
||||
return False
|
||||
|
||||
if exit_code == SOFT_FAIL_EXIT_CODE:
|
||||
logger.info(
|
||||
'{}: Command hook exited with soft failure exit code ({}); skipping remaining actions'.format(
|
||||
config_filename, SOFT_FAIL_EXIT_CODE
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -13,7 +13,7 @@ MONITOR_STATE_TO_CRONHUB = {
|
||||
}
|
||||
|
||||
|
||||
def ping_monitor(ping_url, config_filename, state, dry_run):
|
||||
def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Ping the given Cronhub URL, modified with the monitor.State. Use the given configuration
|
||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
|
||||
@@ -13,7 +13,7 @@ MONITOR_STATE_TO_CRONITOR = {
|
||||
}
|
||||
|
||||
|
||||
def ping_monitor(ping_url, config_filename, state, dry_run):
|
||||
def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Ping the given Cronitor URL, modified with the monitor.State. Use the given configuration
|
||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.hooks import cronhub, cronitor, healthchecks, mysql, postgresql
|
||||
from borgmatic.hooks import cronhub, cronitor, healthchecks, mysql, pagerduty, postgresql
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -8,6 +8,7 @@ HOOK_NAME_TO_MODULE = {
|
||||
'healthchecks': healthchecks,
|
||||
'cronitor': cronitor,
|
||||
'cronhub': cronhub,
|
||||
'pagerduty': pagerduty,
|
||||
'postgresql_databases': postgresql,
|
||||
'mysql_databases': mysql,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from borgmatic.borg.create import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||
|
||||
@@ -83,7 +84,10 @@ def remove_database_dumps(dump_path, databases, database_type_name, log_prefix,
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
os.remove(dump_filename)
|
||||
if os.path.isdir(dump_filename):
|
||||
shutil.rmtree(dump_filename)
|
||||
else:
|
||||
os.remove(dump_filename)
|
||||
dump_file_dir = os.path.dirname(dump_filename)
|
||||
|
||||
if len(os.listdir(dump_file_dir)) == 0:
|
||||
|
||||
@@ -22,13 +22,14 @@ class Forgetful_buffering_handler(logging.Handler):
|
||||
first) once a particular capacity in bytes is reached.
|
||||
'''
|
||||
|
||||
def __init__(self, byte_capacity):
|
||||
def __init__(self, byte_capacity, log_level):
|
||||
super().__init__()
|
||||
|
||||
self.byte_capacity = byte_capacity
|
||||
self.byte_count = 0
|
||||
self.buffer = []
|
||||
self.forgot = False
|
||||
self.setLevel(log_level)
|
||||
|
||||
def emit(self, record):
|
||||
message = record.getMessage() + '\n'
|
||||
@@ -64,16 +65,18 @@ def format_buffered_logs_for_payload():
|
||||
return payload
|
||||
|
||||
|
||||
def ping_monitor(ping_url_or_uuid, config_filename, state, dry_run):
|
||||
def ping_monitor(ping_url_or_uuid, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Ping the given Healthchecks URL or UUID, modified with the monitor.State. Use the given
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||
anything.
|
||||
configuration filename in any log entries, and log to Healthchecks with the giving log level.
|
||||
If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
if state is monitor.State.START:
|
||||
# Add a handler to the root logger that stores in memory the most recent logs emitted. That
|
||||
# way, we can send them all to Healthchecks upon a finish or failure state.
|
||||
logging.getLogger().addHandler(Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES))
|
||||
logging.getLogger().addHandler(
|
||||
Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES, monitoring_log_level)
|
||||
)
|
||||
payload = ''
|
||||
|
||||
ping_url = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub')
|
||||
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub', 'pagerduty')
|
||||
|
||||
|
||||
class State(Enum):
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
|
||||
import requests
|
||||
|
||||
from borgmatic.hooks import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
|
||||
|
||||
|
||||
def ping_monitor(integration_key, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
If this is an error state, create a PagerDuty event with the given integration key. Use the
|
||||
given configuration filename in any log entries. If this is a dry run, then don't actually
|
||||
create an event.
|
||||
'''
|
||||
if state != monitor.State.FAIL:
|
||||
logger.debug(
|
||||
'{}: Ignoring unsupported monitoring {} in PagerDuty hook'.format(
|
||||
config_filename, state.name.lower()
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
|
||||
logger.info('{}: Sending failure event to PagerDuty {}'.format(config_filename, dry_run_label))
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
hostname = platform.node()
|
||||
local_timestamp = (
|
||||
datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc).astimezone().isoformat()
|
||||
)
|
||||
payload = json.dumps(
|
||||
{
|
||||
'routing_key': integration_key,
|
||||
'event_action': 'trigger',
|
||||
'payload': {
|
||||
'summary': 'backup failed on {}'.format(hostname),
|
||||
'severity': 'error',
|
||||
'source': hostname,
|
||||
'timestamp': local_timestamp,
|
||||
'component': 'borgmatic',
|
||||
'group': 'backups',
|
||||
'class': 'backup failure',
|
||||
'custom_details': {
|
||||
'hostname': hostname,
|
||||
'configuration filename': config_filename,
|
||||
'server time': local_timestamp,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
logger.debug('{}: Using PagerDuty payload: {}'.format(config_filename, payload))
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
requests.post(EVENTS_API_URL, data=payload.encode('utf-8'))
|
||||
+9
-2
@@ -110,7 +110,11 @@ def color_text(color, message):
|
||||
|
||||
|
||||
def configure_logging(
|
||||
console_log_level, syslog_log_level=None, log_file_log_level=None, log_file=None
|
||||
console_log_level,
|
||||
syslog_log_level=None,
|
||||
log_file_log_level=None,
|
||||
monitoring_log_level=None,
|
||||
log_file=None,
|
||||
):
|
||||
'''
|
||||
Configure logging to go to both the console and (syslog or log file). Use the given log levels,
|
||||
@@ -122,6 +126,8 @@ def configure_logging(
|
||||
syslog_log_level = console_log_level
|
||||
if log_file_log_level is None:
|
||||
log_file_log_level = console_log_level
|
||||
if monitoring_log_level is None:
|
||||
monitoring_log_level = console_log_level
|
||||
|
||||
# Log certain log levels to console stderr and others to stdout. This supports use cases like
|
||||
# grepping (non-error) output.
|
||||
@@ -160,5 +166,6 @@ def configure_logging(
|
||||
handlers = (console_handler,)
|
||||
|
||||
logging.basicConfig(
|
||||
level=min(console_log_level, syslog_log_level, log_file_log_level), handlers=handlers
|
||||
level=min(console_log_level, syslog_log_level, log_file_log_level, monitoring_log_level),
|
||||
handlers=handlers,
|
||||
)
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM python:3.7.4-alpine3.10 as borgmatic
|
||||
FROM python:3.8.1-alpine3.11 as borgmatic
|
||||
|
||||
COPY . /app
|
||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
@@ -7,7 +7,7 @@ RUN borgmatic --help > /command-line.txt \
|
||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||
|
||||
FROM node:12.10.0-alpine as html
|
||||
FROM node:13.7.0-alpine as html
|
||||
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Security policy
|
||||
permalink: security-policy/index.html
|
||||
---
|
||||
|
||||
## Supported versions
|
||||
|
||||
While we want to hear about security vulnerabilities in all versions of
|
||||
borgmatic, security fixes will only be made to the most recently released
|
||||
version. It's not practical for our small volunteer effort to maintain
|
||||
multiple different release branches and put out separate security patches for
|
||||
each.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
If you find a security vulnerability, please [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) or [send email
|
||||
directly](mailto:witten@torsion.org) as appropriate. You should expect to hear
|
||||
back within a few days at most, and generally sooner.
|
||||
@@ -29,6 +29,10 @@ configuration file, right before the `create` action. `after_backup` hooks run
|
||||
afterwards, but not if an error occurs in a previous hook or in the backups
|
||||
themselves.
|
||||
|
||||
There are additional hooks for the `prune` and `check` actions as well.
|
||||
`before_prune` and `after_prune` run if there are any `prune` actions, while
|
||||
`before_check` and `after_check` run if there are any `check` actions.
|
||||
|
||||
You can also use `before_everything` and `after_everything` hooks to perform
|
||||
global setup or cleanup:
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: How to backup to a removable drive or an intermittent server
|
||||
---
|
||||
## Occasional backups
|
||||
|
||||
A common situation is backing up to a repository that's only sometimes online.
|
||||
For instance, you might send most of your backups to the cloud, but
|
||||
occasionally you want to plug in an external hard drive or backup to your
|
||||
buddy's sometimes-online server for that extra level of redundancy.
|
||||
|
||||
But if you run borgmatic and your hard drive isn't plugged in, or your buddy's
|
||||
server is offline, then you'll get an annoying error message and the overall
|
||||
borgmatic run will fail (even if individual repositories still complete).
|
||||
|
||||
So what if you want borgmatic to swallow the error of a missing drive
|
||||
or an offline server, and continue trucking along? That's where the concept of
|
||||
"soft failure" come in.
|
||||
|
||||
## Soft failure command hooks
|
||||
|
||||
This feature leverages [borgmatic command
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
so first familiarize yourself with them. The idea is that you write a simple
|
||||
test in the form of a borgmatic hook to see if backups should proceed or not.
|
||||
|
||||
The way the test works is that if any of your hook commands return a special
|
||||
exit status of 75, that indicates to borgmatic that it's a temporary failure,
|
||||
and borgmatic should skip all subsequent actions for that configuration file.
|
||||
If you return any other status, then it's a standard success or error. (Zero is
|
||||
success; anything else other than 75 is an error).
|
||||
|
||||
So for instance, if you have an external drive that's only sometimes mounted,
|
||||
declare its repository in its own [separate configuration
|
||||
file](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/),
|
||||
say at `/etc/borgmatic.d/removable.yaml`:
|
||||
|
||||
```yaml
|
||||
location:
|
||||
source_directories:
|
||||
- /home
|
||||
|
||||
repositories:
|
||||
- /mnt/removable/backup.borg
|
||||
```
|
||||
|
||||
Then, write a `before_backup` hook in that same configuration file that uses
|
||||
the external `findmnt` utility to see whether the drive is mounted before
|
||||
proceeding.
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
before_backup:
|
||||
- findmnt /mnt/removable > /dev/null || exit 75
|
||||
```
|
||||
|
||||
What this does is check if the `findmnt` command errors when probing for a
|
||||
particular mount point. If it does error, then it returns exit code 75 to
|
||||
borgmatic. borgmatic logs the soft failure, skips all further actions in that
|
||||
configurable file, and proceeds onward to any other borgmatic configuration
|
||||
files you may have.
|
||||
|
||||
You can imagine a similar check for the sometimes-online server case:
|
||||
|
||||
```yaml
|
||||
location:
|
||||
source_directories:
|
||||
- /home
|
||||
|
||||
repositories:
|
||||
- me@buddys-server.org:backup.borg
|
||||
|
||||
hooks:
|
||||
before_backup:
|
||||
- ping -q -c 1 buddys-server.org > /dev/null || exit 75
|
||||
```
|
||||
|
||||
## Caveats and details
|
||||
|
||||
There are some caveats you should be aware of with this feature.
|
||||
|
||||
* You'll generally want to put a soft failure command in the `before_backup`
|
||||
hook, so as to gate whether the backup action occurs. While a soft failure is
|
||||
also supported in the `after_backup` hook, returning a soft failure there
|
||||
won't prevent any actions from occuring, because they've already occurred!
|
||||
Similiarly, you can return a soft failure from an `on_error` hook, but at
|
||||
that point it's too late to prevent the error.
|
||||
* Returning a soft failure does prevent further commands in the same hook from
|
||||
executing. So, like a standard error, it is an "early out". Unlike a standard
|
||||
error, borgmatic does not display it in angry red text or consider it a
|
||||
failure.
|
||||
* The soft failure only applies to the scope of a single borgmatic
|
||||
configuration file. So put anything that you don't want soft-failed, like
|
||||
always-online cloud backups, in separate configuration files from your
|
||||
soft-failing repositories.
|
||||
* The soft failure doesn't have to apply to a repository. You can even perform
|
||||
a test to make sure that individual source directories are mounted and
|
||||
available. Use your imagination!
|
||||
* The soft failure feature also works for `before_prune`, `after_prune`,
|
||||
`before_check`, and `after_check` hooks. But it is not implemented for
|
||||
`before_everything` or `after_everything`.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
@@ -112,6 +112,12 @@ borgmatic restore --archive host-2019-01-02T04:06:07.080910
|
||||
|
||||
(No borgmatic `restore` action? Upgrade borgmatic!)
|
||||
|
||||
With newer versions of borgmatic, you can simplify this to:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive latest
|
||||
```
|
||||
|
||||
The `--archive` value is the name of the archive to restore from. This
|
||||
restores all databases dumps that borgmatic originally backed up to that
|
||||
archive.
|
||||
|
||||
@@ -31,6 +31,12 @@ borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
||||
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
With newer versions of borgmatic, you can simplify this to:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive latest
|
||||
```
|
||||
|
||||
The `--archive` value is the name of the archive to extract. This extracts the
|
||||
entire contents of the archive to the current directory, so make sure you're
|
||||
in the right place before running the command.
|
||||
@@ -106,6 +112,12 @@ Omit the `--archive` flag to mount all archives (lazy-loaded):
|
||||
borgmatic mount --mount-point /mnt
|
||||
```
|
||||
|
||||
Or use the "latest" value for the archive to mount the latest successful archive:
|
||||
|
||||
```bash
|
||||
borgmatic mount --archive latest --mount-point /mnt
|
||||
```
|
||||
|
||||
If you'd like to restrict the mounted filesystem to only particular paths from
|
||||
your archive, use the `--path` flag, similar to the `extract` action above.
|
||||
For instance:
|
||||
|
||||
@@ -95,7 +95,8 @@ borgmatic --log-file /path/to/file.log
|
||||
```
|
||||
|
||||
Note that if you use the `--log-file` flag, you are responsible for rotating
|
||||
the log file so it doesn't grow too large. Also, there is a
|
||||
the log file so it doesn't grow too large, for example with
|
||||
[logrotate](https://wiki.archlinux.org/index.php/Logrotate). Also, there is a
|
||||
`--log-file-verbosity` flag to customize the log file's log level.
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ for each configuration file one at a time. In other words, borgmatic does not
|
||||
perform any merging of configuration files by default. If you'd like borgmatic
|
||||
to merge your configuration files, see below about configuration includes.
|
||||
|
||||
And if you need even more customizability, you can specify alternate
|
||||
configuration paths on the command-line with borgmatic's `--config` option.
|
||||
See `borgmatic --help` for more information.
|
||||
Additionally, the `~/.config/borgmatic.d/` directory works the same way as
|
||||
`/etc/borgmatic.d`. If you need even more customizability, you can specify
|
||||
alternate configuration paths on the command-line with borgmatic's `--config`
|
||||
flag. See `borgmatic --help` for more information.
|
||||
|
||||
|
||||
## Configuration includes
|
||||
|
||||
@@ -28,14 +28,15 @@ hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hoo
|
||||
below for how to configure this.
|
||||
4. **borgmatic monitoring hooks**: This feature integrates with monitoring
|
||||
services like [Healthchecks](https://healthchecks.io/),
|
||||
[Cronitor](https://cronitor.io), and [Cronhub](https://cronhub.io), and pings
|
||||
these services whenever borgmatic runs. That way, you'll receive an alert when
|
||||
something goes wrong or the service doesn't hear from borgmatic for a
|
||||
configured interval. See
|
||||
[Healthchecks
|
||||
[Cronitor](https://cronitor.io), [Cronhub](https://cronhub.io), and
|
||||
[PagerDuty](https://www.pagerduty.com/) and pings these services whenever
|
||||
borgmatic runs. That way, you'll receive an alert when something goes wrong or
|
||||
(for certain hooks) the service doesn't hear from borgmatic for a configured
|
||||
interval. See [Healthchecks
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook), [Cronitor
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook), and [Cronhub
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook), [Cronhub
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook), and
|
||||
[PagerDuty hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
|
||||
below for how to configure this.
|
||||
3. **Third-party monitoring software**: You can use traditional monitoring
|
||||
software to consume borgmatic JSON output and track when the last
|
||||
@@ -131,9 +132,9 @@ the `on_error` hooks run, also tacking on logs including the error itself. But
|
||||
the logs are only included for errors that occur when a `prune`, `create`, or
|
||||
`check` action is run.
|
||||
|
||||
Note that borgmatic sends logs to Healthchecks by applying the maximum of any
|
||||
other borgmatic verbosity levels (`--verbosity`, `--syslog-verbosity`, etc.),
|
||||
as there is not currently a dedicated Healthchecks verbosity setting.
|
||||
You can customize the verbosity of the logs that are sent to Healthchecks with
|
||||
borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags
|
||||
may also be of use. See `borgmatic --help` for more information.
|
||||
|
||||
You can configure Healthchecks to notify you by a [variety of
|
||||
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
||||
@@ -200,6 +201,32 @@ mechanisms](https://docs.cronhub.io/integrations.html) when backups fail
|
||||
or it doesn't hear from borgmatic for a certain period of time.
|
||||
|
||||
|
||||
## PagerDuty hook
|
||||
|
||||
[PagerDuty](https://www.pagerduty.com/) provides incident monitoring and
|
||||
alerting, and borgmatic has built-in integration with it. Once you create a
|
||||
PagerDuty account and <a
|
||||
href="https://support.pagerduty.com/docs/services-and-integrations">service</a>
|
||||
on their site, all you need to do is configure borgmatic with the unique
|
||||
"Integration Key" for your service. Here's an example:
|
||||
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
pagerduty: a177cad45bd374409f78906a810a3074
|
||||
```
|
||||
|
||||
With this hook in place, borgmatic creates a PagerDuty event for your service
|
||||
whenever backups fail. Specifically, if an error occurs during a `create`,
|
||||
`prune`, or `check` action, borgmatic sends an event to PagerDuty after the
|
||||
`on_error` hooks run. Note that borgmatic does not contact PagerDuty when a
|
||||
backup starts or ends without error.
|
||||
|
||||
You can configure PagerDuty to notify you by a [variety of
|
||||
mechanisms](https://support.pagerduty.com/docs/notifications) when backups
|
||||
fail.
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
@@ -234,6 +261,18 @@ multiple different hosts into a single repository, then you'll need to get
|
||||
fancier with your archive listing. See `borg list --help` for more flags.
|
||||
|
||||
|
||||
### Latest backups
|
||||
|
||||
All borgmatic actions that accept an "--archive" flag allow you to specify an
|
||||
archive name of "latest". This lets you get the latest successful archive
|
||||
without having to first run "borgmatic list" manually, which can be handy in
|
||||
automated scripts. Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic info --archive latest
|
||||
```
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
||||
@@ -68,10 +68,13 @@ sudo generate-borgmatic-config
|
||||
If that command is not found, then it may be installed in a location that's
|
||||
not in your system `PATH` (see above). Try looking in `~/.local/bin/`.
|
||||
|
||||
This generates a sample configuration file at /etc/borgmatic/config.yaml (by
|
||||
default). You should edit the file to suit your needs, as the values are
|
||||
representative. All options are optional except where indicated, so feel free
|
||||
to ignore anything you don't need.
|
||||
This generates a sample configuration file at `/etc/borgmatic/config.yaml` by
|
||||
default. If you'd like to use another path, use the `--destination` flag, for
|
||||
instance: `--destination ~/.config/borgmatic/config.yaml`.
|
||||
|
||||
You should edit the configuration file to suit your needs, as the generated
|
||||
values are only representative. All options are optional except where
|
||||
indicated, so feel free to ignore anything you don't need.
|
||||
|
||||
Note that the configuration file is organized into distinct sections, each
|
||||
with a section name like `location:` or `storage:`. So take care that if you
|
||||
@@ -79,12 +82,11 @@ uncomment a particular option, also uncomment its containing section name, or
|
||||
else borgmatic won't recognize the option. Also be sure to use spaces rather
|
||||
than tabs for indentation; YAML does not allow tabs.
|
||||
|
||||
You can also get the same sample configuration file from the [configuration
|
||||
You can get the same sample configuration file from the [configuration
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the
|
||||
authoritative set of all configuration options. This is handy if borgmatic has
|
||||
added new options
|
||||
since you originally created your configuration file. Also check out how to
|
||||
[upgrade your
|
||||
added new options since you originally created your configuration file. Also
|
||||
check out how to [upgrade your
|
||||
configuration](https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your-configuration).
|
||||
|
||||
|
||||
@@ -173,6 +175,9 @@ The verbosity flag makes borgmatic list the files that it's archiving, which
|
||||
are those that are new or changed since the last backup. Eyeball the list and
|
||||
see if it matches your expectations based on the configuration.
|
||||
|
||||
If you'd like to specify an alternate configuration file path, use the
|
||||
`--config` flag. See `borgmatic --help` for more information.
|
||||
|
||||
|
||||
## Autopilot
|
||||
|
||||
@@ -204,8 +209,7 @@ Then, from the directory where you downloaded them:
|
||||
|
||||
```bash
|
||||
sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/
|
||||
sudo systemctl enable borgmatic.timer
|
||||
sudo systemctl start borgmatic.timer
|
||||
sudo systemctl enable --now borgmatic.timer
|
||||
```
|
||||
|
||||
Feel free to modify the timer file based on how frequently you'd like
|
||||
@@ -214,10 +218,10 @@ borgmatic to run.
|
||||
## Colored output
|
||||
|
||||
Borgmatic produces colored terminal output by default. It is disabled when a
|
||||
non-interactive terminal is detected (like a cron job). Otherwise, you can
|
||||
disable it by passing the `--no-color` flag, setting the environment variable
|
||||
`PY_COLORS=False`, or setting the `color` option to `false` in the `output`
|
||||
section of configuration.
|
||||
non-interactive terminal is detected (like a cron job), or when you use the
|
||||
`--json` flag. Otherwise, you can disable it by passing the `--no-color` flag,
|
||||
setting the environment variable `PY_COLORS=False`, or setting the `color`
|
||||
option to `false` in the `output` section of configuration.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -15,6 +15,8 @@ IOSchedulingPriority=7
|
||||
IOWeight=100
|
||||
|
||||
Restart=no
|
||||
# Prevent rate limiting of borgmatic log events. If you are using an older version of systemd that
|
||||
# doesn't support this (pre-240 or so), you may have to remove this option.
|
||||
LogRateLimitIntervalSec=0
|
||||
|
||||
# Delay start to prevent backups running during boot.
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
python -m pip install --upgrade pip==19.3.1
|
||||
pip install tox==3.14.1
|
||||
python -m pip install --upgrade pip==20.0.2
|
||||
pip install tox==3.14.3
|
||||
export COVERAGE_FILE=/tmp/.coverage
|
||||
tox --workdir /tmp/.tox
|
||||
apk add --no-cache borgbackup postgresql-client mariadb-client
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.4.21'
|
||||
VERSION = '1.5.1'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -98,12 +98,14 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
||||
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
arguments = module.parse_arguments('--stats')
|
||||
arguments = module.parse_arguments('--stats', '--files')
|
||||
|
||||
assert 'prune' in arguments
|
||||
assert arguments['prune'].stats
|
||||
assert arguments['prune'].files
|
||||
assert 'create' in arguments
|
||||
assert arguments['create'].stats
|
||||
assert arguments['create'].files
|
||||
assert 'check' in arguments
|
||||
|
||||
|
||||
@@ -423,6 +425,25 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
|
||||
module.parse_arguments('--stats', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_files_and_create_flags_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--files', 'create', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_files_and_prune_flags_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--files', 'prune', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_files_flag_but_no_create_or_prune_or_restore_flag_raises_value_error():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--files', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_json_with_list_or_info():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
||||
@@ -239,3 +239,28 @@ def test_parse_configuration_applies_overrides():
|
||||
'local_path': 'borg2',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_parse_configuration_applies_normalization():
|
||||
mock_config_and_schema(
|
||||
'''
|
||||
location:
|
||||
source_directories:
|
||||
- /home
|
||||
|
||||
repositories:
|
||||
- hostname.borg
|
||||
|
||||
exclude_if_present: .nobackup
|
||||
'''
|
||||
)
|
||||
|
||||
result = module.parse_configuration('config.yaml', 'schema.yaml')
|
||||
|
||||
assert result == {
|
||||
'location': {
|
||||
'source_directories': ['/home'],
|
||||
'repositories': ['hostname.borg'],
|
||||
'exclude_if_present': ['.nobackup'],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,21 @@ def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag():
|
||||
assert flags == ('--prefix', 'foo-')
|
||||
|
||||
|
||||
def test_check_archives_with_progress_calls_borg_with_progress_parameter():
|
||||
checks = ('repository',)
|
||||
consistency_config = {'check_last': None}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'check', '--progress', 'repo'), error_on_warnings=True
|
||||
).once()
|
||||
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={}, consistency_config=consistency_config, progress=True
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_repair_calls_borg_with_repair_parameter():
|
||||
checks = ('repository',)
|
||||
consistency_config = {'check_last': None}
|
||||
|
||||
@@ -145,9 +145,16 @@ def test_make_exclude_flags_does_not_include_exclude_caches_when_false_in_config
|
||||
|
||||
|
||||
def test_make_exclude_flags_includes_exclude_if_present_when_in_config():
|
||||
exclude_flags = module._make_exclude_flags(location_config={'exclude_if_present': 'exclude_me'})
|
||||
exclude_flags = module._make_exclude_flags(
|
||||
location_config={'exclude_if_present': ['exclude_me', 'also_me']}
|
||||
)
|
||||
|
||||
assert exclude_flags == ('--exclude-if-present', 'exclude_me')
|
||||
assert exclude_flags == (
|
||||
'--exclude-if-present',
|
||||
'exclude_me',
|
||||
'--exclude-if-present',
|
||||
'also_me',
|
||||
)
|
||||
|
||||
|
||||
def test_make_exclude_flags_includes_keep_exclude_tags_when_true_in_config():
|
||||
@@ -295,7 +302,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--stats') + ARCHIVE_WITH_PATHS,
|
||||
('borg', 'create', '--info') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
@@ -349,8 +356,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
('borg', 'create', '--debug', '--show-rc') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
@@ -421,7 +427,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_parameter():
|
||||
def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_parameter():
|
||||
# --dry-run and --stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
@@ -432,8 +438,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
('borg', 'create', '--info', '--dry-run') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
@@ -448,36 +453,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_parameter():
|
||||
# --dry-run and --stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=True,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
stats=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -841,7 +817,7 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
@@ -867,6 +843,86 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--info', '--stats') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
stats=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_files_calls_borg_with_list_parameter_and_warning_output_log_level():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
files=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
files=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
@@ -875,8 +931,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'create', '--info', '--stats', '--progress') + ARCHIVE_WITH_PATHS,
|
||||
error_on_warnings=False,
|
||||
('borg', 'create', '--info', '--progress') + ARCHIVE_WITH_PATHS, error_on_warnings=False
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
|
||||
@@ -7,6 +7,110 @@ from borgmatic.borg import list as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
BORG_LIST_LATEST_ARGUMENTS = (
|
||||
'--glob-archives',
|
||||
module.BORG_EXCLUDE_CHECKPOINTS_GLOB,
|
||||
'--last',
|
||||
'1',
|
||||
'--short',
|
||||
'repo',
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_passes_through_non_latest_archive_name():
|
||||
archive = 'myhost-2030-01-01T14:41:17.647620'
|
||||
|
||||
assert module.resolve_archive_name('repo', archive, storage_config={}) == archive
|
||||
|
||||
|
||||
def test_resolve_archive_name_calls_borg_with_parameters():
|
||||
expected_archive = 'archive-name'
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
|
||||
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_log_info_calls_borg_with_info_parameter():
|
||||
expected_archive = 'archive-name'
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--info') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_log_debug_calls_borg_with_debug_parameter():
|
||||
expected_archive = 'archive-name'
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--debug', '--show-rc') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
assert module.resolve_archive_name('repo', 'latest', storage_config={}) == expected_archive
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_local_path_calls_borg_via_local_path():
|
||||
expected_archive = 'archive-name'
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name('repo', 'latest', storage_config={}, local_path='borg1')
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
expected_archive = 'archive-name'
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--remote-path', 'borg1') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name('repo', 'latest', storage_config={}, remote_path='borg1')
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_archive_name_without_archives_raises():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return('')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.resolve_archive_name('repo', 'latest', storage_config={})
|
||||
|
||||
|
||||
def test_resolve_archive_name_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
expected_archive = 'archive-name'
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--lock-wait', 'okay') + BORG_LIST_LATEST_ARGUMENTS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return(expected_archive + '\n')
|
||||
|
||||
assert (
|
||||
module.resolve_archive_name('repo', 'latest', storage_config={'lock_wait': 'okay'})
|
||||
== expected_archive
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
|
||||
@@ -75,9 +75,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('--stats', '--info', '--list', 'repo'), logging.INFO
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--info', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
@@ -90,9 +88,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo'), logging.INFO
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.prune_archives(
|
||||
@@ -144,7 +140,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
|
||||
def test_prune_archives_with_stats_calls_borg_with_stats_parameter_and_warning_output_log_level():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
@@ -160,6 +156,56 @@ def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_stats_and_log_info_calls_borg_with_stats_parameter_and_info_output_log_level():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
retention_config=retention_config,
|
||||
stats=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_files_calls_borg_with_list_parameter_and_warning_output_log_level():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--list', 'repo'), logging.WARNING)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
retention_config=retention_config,
|
||||
files=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_files_and_log_info_calls_borg_with_list_parameter_and_info_output_log_level():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--info', '--list', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
retention_config=retention_config,
|
||||
files=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
|
||||
storage_config = {'umask': '077'}
|
||||
retention_config = flexmock()
|
||||
|
||||
@@ -3,6 +3,7 @@ import subprocess
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.hooks.command
|
||||
from borgmatic.commands import borgmatic as module
|
||||
|
||||
|
||||
@@ -13,7 +14,7 @@ def test_run_configuration_runs_actions_for_each_repository():
|
||||
expected_results[1:]
|
||||
)
|
||||
config = {'location': {'repositories': ['foo', 'bar']}}
|
||||
arguments = {'global': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1)}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -22,11 +23,11 @@ def test_run_configuration_runs_actions_for_each_repository():
|
||||
|
||||
def test_run_configuration_calls_hooks_for_prune_action():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').never()
|
||||
flexmock(module.command).should_receive('execute_hook').twice()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'prune': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -37,18 +38,18 @@ def test_run_configuration_executes_and_calls_hooks_for_create_action():
|
||||
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
|
||||
def test_run_configuration_calls_hooks_for_check_action():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').never()
|
||||
flexmock(module.command).should_receive('execute_hook').twice()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'check': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'check': flexmock()}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -59,7 +60,7 @@ def test_run_configuration_does_not_trigger_hooks_for_list_action():
|
||||
flexmock(module.dispatch).should_receive('call_hooks').never()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'list': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'list': flexmock()}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -72,7 +73,7 @@ def test_run_configuration_logs_actions_error():
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False)}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -86,13 +87,27 @@ def test_run_configuration_logs_pre_hook_error():
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').never()
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_bails_for_pre_hook_soft_failure():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(error).and_return(None)
|
||||
flexmock(module).should_receive('make_error_log_records').never()
|
||||
flexmock(module).should_receive('run_actions').never()
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_run_configuration_logs_post_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
|
||||
@@ -103,13 +118,30 @@ def test_run_configuration_logs_post_hook_error():
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_bails_for_post_hook_soft_failure():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
|
||||
error
|
||||
).and_return(None)
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(module).should_receive('make_error_log_records').never()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_run_configuration_logs_on_error_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
|
||||
@@ -119,7 +151,22 @@ def test_run_configuration_logs_on_error_hook_error():
|
||||
).and_return(expected_results[1:])
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_bails_for_on_error_hook_soft_failure():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(error)
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
@@ -228,7 +275,7 @@ def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
|
||||
def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
@@ -305,7 +352,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(ValueError)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
@@ -319,7 +366,7 @@ def test_collect_configuration_run_summary_logs_post_hook_error():
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
|
||||
@@ -21,6 +21,14 @@ def test_get_default_config_paths_prefers_xdg_config_home_for_user_config_path()
|
||||
assert '/home/user/.etc/borgmatic/config.yaml' in config_paths
|
||||
|
||||
|
||||
def test_get_default_config_paths_does_not_expand_home_when_false():
|
||||
flexmock(module.os, environ={'HOME': '/home/user'})
|
||||
|
||||
config_paths = module.get_default_config_paths(expand_home=False)
|
||||
|
||||
assert '$HOME/.config/borgmatic/config.yaml' in config_paths
|
||||
|
||||
|
||||
def test_collect_config_filenames_collects_given_files():
|
||||
config_paths = ('config.yaml', 'other.yaml')
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(False)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
|
||||
from borgmatic.config import normalize as module
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'config,expected_config',
|
||||
(
|
||||
(
|
||||
{'location': {'exclude_if_present': '.nobackup'}},
|
||||
{'location': {'exclude_if_present': ['.nobackup']}},
|
||||
),
|
||||
(
|
||||
{'location': {'exclude_if_present': ['.nobackup']}},
|
||||
{'location': {'exclude_if_present': ['.nobackup']}},
|
||||
),
|
||||
(
|
||||
{'location': {'source_directories': ['foo', 'bar']}},
|
||||
{'location': {'source_directories': ['foo', 'bar']}},
|
||||
),
|
||||
({'storage': {'compression': 'yes_please'}}, {'storage': {'compression': 'yes_please'}}),
|
||||
),
|
||||
)
|
||||
def test_normalize_applies_hard_coded_normalization_to_config(config, expected_config):
|
||||
module.normalize(config)
|
||||
|
||||
assert config == expected_config
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
@@ -79,3 +80,19 @@ def test_execute_hook_on_error_logs_as_error():
|
||||
).once()
|
||||
|
||||
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
|
||||
|
||||
|
||||
def test_considered_soft_failure_treats_soft_fail_exit_code_as_soft_fail():
|
||||
error = subprocess.CalledProcessError(module.SOFT_FAIL_EXIT_CODE, 'try again')
|
||||
|
||||
assert module.considered_soft_failure('config.yaml', error)
|
||||
|
||||
|
||||
def test_considered_soft_failure_does_not_treat_other_exit_code_as_soft_fail():
|
||||
error = subprocess.CalledProcessError(1, 'error')
|
||||
|
||||
assert not module.considered_soft_failure('config.yaml', error)
|
||||
|
||||
|
||||
def test_considered_soft_failure_does_not_treat_other_exception_type_as_soft_fail():
|
||||
assert not module.considered_soft_failure('config.yaml', Exception())
|
||||
|
||||
@@ -7,32 +7,42 @@ def test_ping_monitor_rewrites_ping_url_for_start_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/start/abcdef')
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
|
||||
ping_url = 'https://example.com/ping/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/start/abcdef')
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_rewrites_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/finish/abcdef')
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FINISH, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.FINISH, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_rewrites_ping_url_for_fail_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/fail/abcdef')
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FAIL, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=True)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=True
|
||||
)
|
||||
|
||||
@@ -7,25 +7,33 @@ def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'run'))
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'complete'))
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FINISH, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.FINISH, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'fail'))
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FAIL, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=True)
|
||||
module.ping_monitor(
|
||||
ping_url, 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=True
|
||||
)
|
||||
|
||||
@@ -66,6 +66,7 @@ def test_remove_database_dumps_removes_dump_for_each_database():
|
||||
'databases', 'bar', None
|
||||
).and_return('databases/localhost/bar')
|
||||
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(False)
|
||||
flexmock(module.os).should_receive('remove').with_args('databases/localhost/foo').once()
|
||||
flexmock(module.os).should_receive('remove').with_args('databases/localhost/bar').once()
|
||||
flexmock(module.os).should_receive('listdir').with_args('databases/localhost').and_return(
|
||||
@@ -77,6 +78,21 @@ def test_remove_database_dumps_removes_dump_for_each_database():
|
||||
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_removes_dump_in_directory_format():
|
||||
databases = [{'name': 'foo'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').with_args(
|
||||
'databases', 'foo', None
|
||||
).and_return('databases/localhost/foo')
|
||||
|
||||
flexmock(module.os.path).should_receive('isdir').and_return(True)
|
||||
flexmock(module.os).should_receive('remove').never()
|
||||
flexmock(module.shutil).should_receive('rmtree').with_args('databases/localhost/foo').once()
|
||||
flexmock(module.os).should_receive('listdir').with_args('databases/localhost').and_return([])
|
||||
flexmock(module.os).should_receive('rmdir').with_args('databases/localhost').once()
|
||||
|
||||
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_with_dry_run_skips_removal():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module.os).should_receive('rmdir').never()
|
||||
|
||||
@@ -4,7 +4,7 @@ from borgmatic.hooks import healthchecks as module
|
||||
|
||||
|
||||
def test_forgetful_buffering_handler_emit_collects_log_records():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_forgetful_buffering_handler_emit_collects_log_records():
|
||||
|
||||
|
||||
def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reached():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=len('foo\nbar\n'))
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=len('foo\nbar\n'), log_level=1)
|
||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||
assert handler.buffer == ['foo\n']
|
||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||
@@ -26,7 +26,7 @@ def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reac
|
||||
|
||||
|
||||
def test_format_buffered_logs_for_payload_flattens_log_buffer():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
||||
handler.buffer = ['foo\n', 'bar\n']
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_format_buffered_logs_for_payload_flattens_log_buffer():
|
||||
|
||||
|
||||
def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs_forgotten():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
||||
handler.buffer = ['foo\n', 'bar\n']
|
||||
handler.forgot = True
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
|
||||
@@ -63,7 +63,13 @@ def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
'{}/{}'.format(ping_url, 'start'), data=''.encode('utf-8')
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url,
|
||||
'config.yaml',
|
||||
state=module.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
@@ -74,7 +80,13 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
ping_url, data=payload.encode('utf-8')
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.FINISH, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url,
|
||||
'config.yaml',
|
||||
state=module.monitor.State.FINISH,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
@@ -85,7 +97,13 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
'{}/{}'.format(ping_url, 'fail'), data=payload.encode('utf')
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.FAIL, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_url,
|
||||
'config.yaml',
|
||||
state=module.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
||||
@@ -96,7 +114,13 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
||||
'https://hc-ping.com/{}'.format(ping_uuid), data=payload.encode('utf-8')
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_uuid, 'config.yaml', state=module.monitor.State.FINISH, dry_run=False)
|
||||
module.ping_monitor(
|
||||
ping_uuid,
|
||||
'config.yaml',
|
||||
state=module.monitor.State.FINISH,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
@@ -104,4 +128,10 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=True)
|
||||
module.ping_monitor(
|
||||
ping_url,
|
||||
'config.yaml',
|
||||
state=module.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import pagerduty as module
|
||||
|
||||
|
||||
def test_ping_monitor_ignores_start_state():
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(
|
||||
'abc123', 'config.yaml', module.monitor.State.START, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_ignores_finish_state():
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(
|
||||
'abc123', 'config.yaml', module.monitor.State.FINISH, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_calls_api_for_fail_state():
|
||||
flexmock(module.requests).should_receive('post')
|
||||
|
||||
module.ping_monitor(
|
||||
'abc123', 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=False
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_call_api():
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(
|
||||
'abc123', 'config.yaml', module.monitor.State.FAIL, monitoring_log_level=1, dry_run=True
|
||||
)
|
||||
Reference in New Issue
Block a user