mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 18:13:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc9482970 | ||
|
|
57ffad4e04 | ||
|
|
5422d14f93 | ||
|
|
e6d8c736d0 | ||
|
|
18d3542fbc | ||
|
|
93f453cecf | ||
|
|
505bb778fa | ||
|
|
b09d464162 | ||
|
|
a9104ed090 | ||
|
|
06f134cc71 | ||
|
|
584359b6c0 | ||
|
|
26a1a3d1e0 | ||
|
|
6da05cbe2d | ||
|
|
f48f52079d | ||
|
|
76c569cf84 | ||
|
|
b121290c0f | ||
|
|
8fd46b8c70 | ||
|
|
603f525352 | ||
|
|
8c8640d0ab | ||
|
|
e3dd545345 | ||
|
|
589fc30fc8 | ||
|
|
bd3c51fc5a | ||
|
|
2c46f53ef6 | ||
|
|
939f4d4e3d | ||
|
|
3006db0cae | ||
|
|
22640a9ca0 | ||
|
|
ca23c3b8b3 | ||
|
|
74607fdd43 | ||
|
|
b53684a8f0 | ||
|
|
f055f5dea8 | ||
|
|
4dc4fe0b8d | ||
|
|
5e3c2da79c | ||
|
|
37dc94bc79 | ||
|
|
fc274b43f0 | ||
|
|
9ab12e4312 | ||
|
|
a5ff35c198 | ||
|
|
458e7776c5 | ||
|
|
fa5fa1c11b | ||
|
|
f8bc67be8d | ||
|
|
17586d49ac | ||
|
|
2f75c9aa9e | ||
|
|
60650ccfc7 | ||
|
|
c12c47cace | ||
|
|
d6aaab8a09 | ||
|
|
128ebf04ce | ||
|
|
b1941bcce9 | ||
|
|
7b3b28616d | ||
|
|
f3910f49ca | ||
|
|
59e1cac92c | ||
|
|
b1f0287fdb | ||
|
|
99c35d4077 | ||
|
|
07b9ff61f2 | ||
|
|
f573c1810a | ||
|
|
1d37b14356 |
+10
@@ -40,6 +40,16 @@ steps:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-8-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.8-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: documentation
|
||||
|
||||
steps:
|
||||
|
||||
+4
-3
@@ -13,10 +13,11 @@ module.exports = function(eleventyConfig) {
|
||||
html: true,
|
||||
breaks: false,
|
||||
linkify: true,
|
||||
// Replace links to .md files with links to directories. This allows unparsed Markdown links
|
||||
// to work on GitHub, while rendered links elsewhere also work.
|
||||
replaceLink: function (link, env) {
|
||||
return link.replace(/\.md$/, '/');
|
||||
if (process.env.NODE_ENV == "production") {
|
||||
return link;
|
||||
}
|
||||
return link.replace('https://torsion.org/borgmatic/', 'http://localhost:8080/');
|
||||
}
|
||||
};
|
||||
let markdownItAnchorOptions = {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
.coverage
|
||||
.pytest_cache
|
||||
.tox
|
||||
__pycache__
|
||||
build/
|
||||
dist/
|
||||
pip-wheel-metadata/
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
1.4.6
|
||||
* Verbosity level "-1" for even quieter output: Errors only (#236).
|
||||
|
||||
1.4.5
|
||||
* Log to file instead of syslog via command-line "--log-file" flag (#233).
|
||||
|
||||
1.4.4
|
||||
* #234: Support for Borg --keep-exclude-tags and --exclude-nodump options.
|
||||
|
||||
1.4.3
|
||||
* Monitor backups with Cronitor hook integration. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook
|
||||
|
||||
1.4.2
|
||||
* Extract files to a particular directory via "borgmatic extract --destination" flag.
|
||||
* Rename "borgmatic extract --restore-path" flag to "--path" to reduce confusion with the separate
|
||||
"borgmatic restore" action. Any uses of "--restore-path" will continue working.
|
||||
|
||||
1.4.1
|
||||
* #229: Restore backed up PostgreSQL databases via "borgmatic restore" action. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/
|
||||
* Documentation on how to develop borgmatic's documentation:
|
||||
https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/#documentation-development
|
||||
|
||||
1.4.0
|
||||
* #225: Database dump hooks for PostgreSQL, so you can easily dump your databases before backups
|
||||
run.
|
||||
* #230: Rename "borgmatic list --pattern-from" flag to "--patterns-from" to match Borg.
|
||||
|
||||
1.3.26
|
||||
* #224: Fix "borgmatic list --successful" with a slightly better heuristic for listing successful
|
||||
(non-checkpoint) archives.
|
||||
|
||||
1.3.25
|
||||
* #223: Dead man's switch to detect when backups start failing silently, implemented via
|
||||
healthchecks.io hook integration. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook
|
||||
* Documentation on monitoring and alerting options for borgmatic backups:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/
|
||||
* Automatically rewrite links when developing on documentation locally.
|
||||
|
||||
1.3.24
|
||||
* #86: Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives.
|
||||
* Add a suggestion form to all documentation pages, so users can submit ideas for improving the
|
||||
documentation.
|
||||
* Update documentation link to community Arch Linux borgmatic package.
|
||||
|
||||
1.3.23
|
||||
* #174: More detailed error alerting via runtime context available in "on_error" hook.
|
||||
|
||||
1.3.22
|
||||
* #144: When backups to one of several repositories fails, keep backing up to the other
|
||||
repositories and report errors afterwards.
|
||||
|
||||
@@ -41,10 +41,18 @@ retention:
|
||||
keep_monthly: 6
|
||||
|
||||
consistency:
|
||||
# List of consistency checks to run: "repository", "archives", or both.
|
||||
# List of consistency checks to run: "repository", "archives", etc.
|
||||
checks:
|
||||
- repository
|
||||
- archives
|
||||
|
||||
hooks:
|
||||
# Preparation scripts to run, databases to dump, and monitoring to perform.
|
||||
before_backup:
|
||||
- prepare-for-backup.sh
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
|
||||
```
|
||||
|
||||
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
|
||||
@@ -63,7 +71,9 @@ href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [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/)
|
||||
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
@@ -116,8 +126,3 @@ your thing. In general, contributions are very welcome. We don't bite!
|
||||
Also, please check out the [borgmatic development
|
||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||
info on cloning source code, running tests, etc.
|
||||
|
||||
<script>
|
||||
var links = document.getElementsByClassName("referral");
|
||||
links[Math.floor(Math.random() * links.length)].style.display = "none";
|
||||
</script>
|
||||
|
||||
@@ -60,8 +60,8 @@ def _write_pattern_file(patterns=None):
|
||||
|
||||
def _make_pattern_flags(location_config, pattern_filename=None):
|
||||
'''
|
||||
Given a location config dict with a potential pattern_from option, and a filename containing any
|
||||
additional patterns, return the corresponding Borg flags for those files as a tuple.
|
||||
Given a location config dict with a potential patterns_from option, and a filename containing
|
||||
any additional patterns, return the corresponding Borg flags for those files as a tuple.
|
||||
'''
|
||||
pattern_filenames = tuple(location_config.get('patterns_from') or ()) + (
|
||||
(pattern_filename,) if pattern_filename else ()
|
||||
@@ -90,8 +90,32 @@ 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 ()
|
||||
keep_exclude_tags_flags = (
|
||||
('--keep-exclude-tags',) if location_config.get('keep_exclude_tags') else ()
|
||||
)
|
||||
exclude_nodump_flags = ('--exclude-nodump',) if location_config.get('exclude_nodump') else ()
|
||||
|
||||
return exclude_from_flags + caches_flag + if_present_flags
|
||||
return (
|
||||
exclude_from_flags
|
||||
+ caches_flag
|
||||
+ if_present_flags
|
||||
+ keep_exclude_tags_flags
|
||||
+ exclude_nodump_flags
|
||||
)
|
||||
|
||||
|
||||
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
|
||||
|
||||
|
||||
def borgmatic_source_directories():
|
||||
'''
|
||||
Return a list of borgmatic-specific source directories used for state like database backups.
|
||||
'''
|
||||
return (
|
||||
[BORGMATIC_SOURCE_DIRECTORY]
|
||||
if os.path.exists(os.path.expanduser(BORGMATIC_SOURCE_DIRECTORY))
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
def create_archive(
|
||||
@@ -109,7 +133,9 @@ def create_archive(
|
||||
Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
|
||||
storage config dict, create a Borg archive and return Borg's JSON output (if any).
|
||||
'''
|
||||
sources = _expand_directories(location_config['source_directories'])
|
||||
sources = _expand_directories(
|
||||
location_config['source_directories'] + borgmatic_source_directories()
|
||||
)
|
||||
|
||||
pattern_file = _write_pattern_file(location_config.get('patterns'))
|
||||
exclude_file = _write_pattern_file(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
@@ -47,24 +48,26 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
|
||||
)
|
||||
)
|
||||
|
||||
execute_command(full_extract_command)
|
||||
execute_command(full_extract_command, working_directory=None, error_on_warnings=True)
|
||||
|
||||
|
||||
def extract_archive(
|
||||
dry_run,
|
||||
repository,
|
||||
archive,
|
||||
restore_paths,
|
||||
paths,
|
||||
location_config,
|
||||
storage_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
destination_path=None,
|
||||
progress=False,
|
||||
):
|
||||
'''
|
||||
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
|
||||
restore from the archive, and location/storage configuration dicts, extract the archive into the
|
||||
current directory.
|
||||
restore from the archive, location/storage configuration dicts, optional local and remote Borg
|
||||
paths, and an optional destination path to extract to, extract the archive into the current
|
||||
directory.
|
||||
'''
|
||||
umask = storage_config.get('umask', None)
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
@@ -79,14 +82,18 @@ def extract_archive(
|
||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ ('::'.join((repository, archive)),)
|
||||
+ (tuple(restore_paths) if restore_paths else ())
|
||||
+ ('::'.join((os.path.abspath(repository), archive)),)
|
||||
+ (tuple(paths) if paths else ())
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
execute_command_without_capture(
|
||||
full_command, working_directory=destination_path, error_on_warnings=True
|
||||
)
|
||||
return
|
||||
|
||||
execute_command(full_command)
|
||||
# Error on warnings, as Borg only gives a warning if the restore paths don't exist in the
|
||||
# archive!
|
||||
execute_command(full_command, working_directory=destination_path, error_on_warnings=True)
|
||||
|
||||
+10
-1
@@ -6,6 +6,11 @@ from borgmatic.execute import execute_command
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# A hack to convince Borg to exclude archives ending in ".checkpoint". This assumes that a
|
||||
# non-checkpoint archive name ends in a digit (e.g. from a timestamp).
|
||||
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
|
||||
|
||||
|
||||
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
|
||||
@@ -13,6 +18,8 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
|
||||
if an archive name is given, listing the files in that archive.
|
||||
'''
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
if list_arguments.successful:
|
||||
list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB
|
||||
|
||||
full_command = (
|
||||
(local_path, 'list')
|
||||
@@ -28,7 +35,9 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
|
||||
)
|
||||
+ make_flags('remote-path', remote_path)
|
||||
+ make_flags('lock-wait', lock_wait)
|
||||
+ make_flags_from_arguments(list_arguments, excludes=('repository', 'archive'))
|
||||
+ make_flags_from_arguments(
|
||||
list_arguments, excludes=('repository', 'archive', 'successful')
|
||||
)
|
||||
+ (
|
||||
'::'.join((repository, list_arguments.archive))
|
||||
if list_arguments.archive
|
||||
|
||||
@@ -9,6 +9,7 @@ SUBPARSER_ALIASES = {
|
||||
'create': ['--create', '-C'],
|
||||
'check': ['--check', '-k'],
|
||||
'extract': ['--extract', '-x'],
|
||||
'restore': ['--restore', '-r'],
|
||||
'list': ['--list', '-l'],
|
||||
'info': ['--info', '-i'],
|
||||
}
|
||||
@@ -137,16 +138,29 @@ def parse_arguments(*unparsed_arguments):
|
||||
'-v',
|
||||
'--verbosity',
|
||||
type=int,
|
||||
choices=range(0, 3),
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
|
||||
help='Display verbose progress to the console (from none to lots: 0, 1, or 2) or only errors (-1)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--syslog-verbosity',
|
||||
type=int,
|
||||
choices=range(0, 3),
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
|
||||
help='Log verbose progress to syslog (from none to lots: 0, 1, or 2) or only errors (-1). Ignored when console is interactive or --log-file is given',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file-verbosity',
|
||||
type=int,
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to log file (from none to lots: 0, 1, or 2) or only errors (-1). Only used when --log-file is given',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file',
|
||||
type=str,
|
||||
default=None,
|
||||
help='Write log messages to this file instead of syslog',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--version',
|
||||
@@ -263,7 +277,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
extract_parser = subparsers.add_parser(
|
||||
'extract',
|
||||
aliases=SUBPARSER_ALIASES['extract'],
|
||||
help='Extract a named archive to the current directory',
|
||||
help='Extract files from a named archive to the current directory',
|
||||
description='Extract a named archive to the current directory',
|
||||
add_help=False,
|
||||
)
|
||||
@@ -272,12 +286,20 @@ 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 operate on', required=True)
|
||||
extract_group.add_argument('--archive', help='Name of archive to extract', required=True)
|
||||
extract_group.add_argument(
|
||||
'--path',
|
||||
'--restore-path',
|
||||
metavar='PATH',
|
||||
nargs='+',
|
||||
dest='restore_paths',
|
||||
help='Paths to restore from archive, defaults to the entire archive',
|
||||
dest='paths',
|
||||
help='Paths to extract from archive, defaults to the entire archive',
|
||||
)
|
||||
extract_group.add_argument(
|
||||
'--destination',
|
||||
metavar='PATH',
|
||||
dest='destination',
|
||||
help='Directory to extract files into, defaults to the current directory',
|
||||
)
|
||||
extract_group.add_argument(
|
||||
'--progress',
|
||||
@@ -290,6 +312,37 @@ def parse_arguments(*unparsed_arguments):
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
restore_parser = subparsers.add_parser(
|
||||
'restore',
|
||||
aliases=SUBPARSER_ALIASES['restore'],
|
||||
help='Restore database dumps from a named archive',
|
||||
description='Restore database dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
|
||||
add_help=False,
|
||||
)
|
||||
restore_group = restore_parser.add_argument_group('restore arguments')
|
||||
restore_group.add_argument(
|
||||
'--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(
|
||||
'--database',
|
||||
metavar='NAME',
|
||||
nargs='+',
|
||||
dest='databases',
|
||||
help='Names of databases to restore from archive, defaults to all databases. Note that any databases to restore must be defined in borgmatic\'s configuration',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--progress',
|
||||
dest='progress',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display progress for each database dump file as it is extracted from archive',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
list_parser = subparsers.add_parser(
|
||||
'list',
|
||||
aliases=SUBPARSER_ALIASES['list'],
|
||||
@@ -316,6 +369,12 @@ def parse_arguments(*unparsed_arguments):
|
||||
list_group.add_argument(
|
||||
'-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--successful',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Only list archive names of successful (non-checkpoint) backups',
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||
)
|
||||
@@ -323,7 +382,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
'--first', metavar='N', help='List first N archives after other filters are applied'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--last', metavar='N', help='List first N archives after other filters are applied'
|
||||
'--last', metavar='N', help='List last N archives after other filters are applied'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
|
||||
@@ -333,7 +392,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
)
|
||||
list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
|
||||
list_group.add_argument(
|
||||
'--pattern-from',
|
||||
'--patterns-from',
|
||||
metavar='FILENAME',
|
||||
help='Include or exclude paths matching patterns from pattern file, one per line',
|
||||
)
|
||||
@@ -388,6 +447,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
if 'init' in arguments and arguments['global'].dry_run:
|
||||
raise ValueError('The init action cannot be used with the --dry-run option')
|
||||
|
||||
if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful:
|
||||
raise ValueError('The --glob-archives and --successful options cannot be used together')
|
||||
|
||||
if (
|
||||
'list' in arguments
|
||||
and 'info' in arguments
|
||||
|
||||
+100
-19
@@ -8,7 +8,6 @@ from subprocess import CalledProcessError
|
||||
import colorama
|
||||
import pkg_resources
|
||||
|
||||
from borgmatic import hook
|
||||
from borgmatic.borg import check as borg_check
|
||||
from borgmatic.borg import create as borg_create
|
||||
from borgmatic.borg import environment as borg_environment
|
||||
@@ -19,6 +18,7 @@ from borgmatic.borg import list as borg_list
|
||||
from borgmatic.borg import prune as borg_prune
|
||||
from borgmatic.commands.arguments import parse_arguments
|
||||
from borgmatic.config import checks, collect, convert, validate
|
||||
from borgmatic.hooks import command, cronitor, healthchecks, postgresql
|
||||
from borgmatic.logger import configure_logging, should_do_markup
|
||||
from borgmatic.signals import configure_signals
|
||||
from borgmatic.verbosity import verbosity_to_log_level
|
||||
@@ -48,19 +48,29 @@ def run_configuration(config_filename, config, arguments):
|
||||
local_path = location.get('local_path', 'borg')
|
||||
remote_path = location.get('remote_path')
|
||||
borg_environment.initialize(storage)
|
||||
encountered_error = False
|
||||
encountered_error = None
|
||||
error_repository = ''
|
||||
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
hook.execute_hook(
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'run'
|
||||
)
|
||||
command.execute_hook(
|
||||
hooks.get('before_backup'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'pre-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
postgresql.dump_databases(
|
||||
hooks.get('postgresql_databases'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = True
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running pre-backup hook'.format(config_filename), error
|
||||
)
|
||||
@@ -74,39 +84,59 @@ def run_configuration(config_filename, config, arguments):
|
||||
storage=storage,
|
||||
retention=retention,
|
||||
consistency=consistency,
|
||||
hooks=hooks,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
repository_path=repository_path,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = True
|
||||
except (OSError, CalledProcessError, ValueError) as error:
|
||||
encountered_error = error
|
||||
error_repository = repository_path
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running actions for repository'.format(repository_path), error
|
||||
)
|
||||
|
||||
if 'create' in arguments and not encountered_error:
|
||||
try:
|
||||
hook.execute_hook(
|
||||
postgresql.remove_database_dumps(
|
||||
hooks.get('postgresql_databases'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
command.execute_hook(
|
||||
hooks.get('after_backup'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'post-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'complete'
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = True
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running post-backup hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
if encountered_error:
|
||||
try:
|
||||
hook.execute_hook(
|
||||
command.execute_hook(
|
||||
hooks.get('on_error'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'on-error',
|
||||
global_arguments.dry_run,
|
||||
repository=error_repository,
|
||||
error=encountered_error,
|
||||
output=getattr(encountered_error, 'output', ''),
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'fail'
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
yield from make_error_log_records(
|
||||
@@ -121,6 +151,7 @@ def run_actions(
|
||||
storage,
|
||||
retention,
|
||||
consistency,
|
||||
hooks,
|
||||
local_path,
|
||||
remote_path,
|
||||
repository_path
|
||||
@@ -131,6 +162,9 @@ def run_actions(
|
||||
from the command-line arguments on the given repository.
|
||||
|
||||
Yield JSON output strings from executing any actions that produce JSON.
|
||||
|
||||
Raise OSError or subprocess.CalledProcessError if an error occurs running a command for an
|
||||
action. Raise ValueError if the arguments or configuration passed to action are invalid.
|
||||
'''
|
||||
repository = os.path.expanduser(repository_path)
|
||||
global_arguments = arguments['global']
|
||||
@@ -190,13 +224,52 @@ def run_actions(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
arguments['extract'].archive,
|
||||
arguments['extract'].restore_paths,
|
||||
arguments['extract'].paths,
|
||||
location,
|
||||
storage,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
destination_path=arguments['extract'].destination,
|
||||
progress=arguments['extract'].progress,
|
||||
)
|
||||
if 'restore' in arguments:
|
||||
if arguments['restore'].repository is None or repository == arguments['restore'].repository:
|
||||
logger.info(
|
||||
'{}: Restoring databases from archive {}'.format(
|
||||
repository, arguments['restore'].archive
|
||||
)
|
||||
)
|
||||
|
||||
restore_names = arguments['restore'].databases or []
|
||||
if 'all' in restore_names:
|
||||
restore_names = []
|
||||
|
||||
# Extract dumps for the named databases from the archive.
|
||||
dump_patterns = postgresql.make_database_dump_patterns(restore_names)
|
||||
borg_extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
arguments['restore'].archive,
|
||||
postgresql.convert_glob_patterns_to_borg_patterns(dump_patterns),
|
||||
location,
|
||||
storage,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
destination_path='/',
|
||||
progress=arguments['restore'].progress,
|
||||
)
|
||||
|
||||
# Map the restore names to the corresponding database configurations.
|
||||
databases = list(
|
||||
postgresql.get_database_configurations(
|
||||
hooks.get('postgresql_databases'),
|
||||
restore_names or postgresql.get_database_names_from_dumps(dump_patterns),
|
||||
)
|
||||
)
|
||||
|
||||
# Finally, restore the databases and cleanup the dumps.
|
||||
postgresql.restore_database_dumps(databases, repository, global_arguments.dry_run)
|
||||
postgresql.remove_database_dumps(databases, repository, global_arguments.dry_run)
|
||||
if 'list' in arguments:
|
||||
if arguments['list'].repository is None or repository == arguments['list'].repository:
|
||||
logger.info('{}: Listing archives'.format(repository))
|
||||
@@ -275,9 +348,10 @@ def make_error_log_records(message, error=None):
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
)
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output)
|
||||
)
|
||||
if error.output:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output)
|
||||
)
|
||||
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error))
|
||||
except (ValueError, OSError) as error:
|
||||
yield logging.makeLogRecord(
|
||||
@@ -325,7 +399,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
hooks = config.get('hooks', {})
|
||||
hook.execute_hook(
|
||||
command.execute_hook(
|
||||
hooks.get('before_everything'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
@@ -365,7 +439,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
hooks = config.get('hooks', {})
|
||||
hook.execute_hook(
|
||||
command.execute_hook(
|
||||
hooks.get('after_everything'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
@@ -410,10 +484,17 @@ def main(): # pragma: no cover
|
||||
configs, parse_logs = load_configurations(config_filenames)
|
||||
|
||||
colorama.init(autoreset=True, strip=not should_do_markup(global_arguments.no_color, configs))
|
||||
configure_logging(
|
||||
verbosity_to_log_level(global_arguments.verbosity),
|
||||
verbosity_to_log_level(global_arguments.syslog_verbosity),
|
||||
)
|
||||
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),
|
||||
global_arguments.log_file,
|
||||
)
|
||||
except (FileNotFoundError, PermissionError) as error:
|
||||
configure_logging(logging.CRITICAL)
|
||||
logger.critical('Error configuring logging: {}'.format(error))
|
||||
exit_with_help_link()
|
||||
|
||||
logger.debug('Ensuring legacy configuration is upgraded')
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||
|
||||
@@ -54,10 +54,10 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
|
||||
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
|
||||
|
||||
# Add comments to each section, and then add comments to the fields in each section.
|
||||
generate.add_comments_to_configuration(destination_config, schema)
|
||||
generate.add_comments_to_configuration_map(destination_config, schema)
|
||||
|
||||
for section_name, section_config in destination_config.items():
|
||||
generate.add_comments_to_configuration(
|
||||
generate.add_comments_to_configuration_map(
|
||||
section_config, schema['map'][section_name], indent=generate.INDENT
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from ruamel import yaml
|
||||
|
||||
INDENT = 4
|
||||
SEQUENCE_INDENT = 2
|
||||
|
||||
|
||||
def _insert_newline_before_comment(config, field_name):
|
||||
@@ -15,7 +18,7 @@ def _insert_newline_before_comment(config, field_name):
|
||||
)
|
||||
|
||||
|
||||
def _schema_to_sample_configuration(schema, level=0):
|
||||
def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
'''
|
||||
Given a loaded configuration schema, generate and return sample config for it. Include comments
|
||||
for each section based on the schema "desc" description.
|
||||
@@ -24,14 +27,29 @@ def _schema_to_sample_configuration(schema, level=0):
|
||||
if example is not None:
|
||||
return example
|
||||
|
||||
config = yaml.comments.CommentedMap(
|
||||
[
|
||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
||||
for section_name, section_schema in schema['map'].items()
|
||||
]
|
||||
)
|
||||
|
||||
add_comments_to_configuration(config, schema, indent=(level * INDENT))
|
||||
if 'seq' in schema:
|
||||
config = yaml.comments.CommentedSeq(
|
||||
[
|
||||
_schema_to_sample_configuration(item_schema, level, parent_is_sequence=True)
|
||||
for item_schema in schema['seq']
|
||||
]
|
||||
)
|
||||
add_comments_to_configuration_sequence(
|
||||
config, schema, indent=(level * INDENT) + SEQUENCE_INDENT
|
||||
)
|
||||
elif 'map' in schema:
|
||||
config = yaml.comments.CommentedMap(
|
||||
[
|
||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
||||
for section_name, section_schema in schema['map'].items()
|
||||
]
|
||||
)
|
||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
||||
add_comments_to_configuration_map(
|
||||
config, schema, indent=indent, skip_first=parent_is_sequence
|
||||
)
|
||||
else:
|
||||
raise ValueError('Schema at level {} is unsupported: {}'.format(level, schema))
|
||||
|
||||
return config
|
||||
|
||||
@@ -42,13 +60,12 @@ def _comment_out_line(line):
|
||||
if not stripped_line or stripped_line.startswith('#'):
|
||||
return line
|
||||
|
||||
# Comment out the names of optional sections.
|
||||
one_indent = ' ' * INDENT
|
||||
if not line.startswith(one_indent):
|
||||
return '# ' + line
|
||||
# Comment out the names of optional sections, inserting the '#' after any indent for aesthetics.
|
||||
matches = re.match(r'(\s*)', line)
|
||||
indent_spaces = matches.group(0) if matches else ''
|
||||
count_indent_spaces = len(indent_spaces)
|
||||
|
||||
# Otherwise, comment out the line, but insert the "#" after the first indent for aesthetics.
|
||||
return '# '.join((one_indent, line[INDENT:]))
|
||||
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
||||
|
||||
|
||||
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
||||
@@ -90,7 +107,12 @@ def _render_configuration(config):
|
||||
'''
|
||||
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
||||
'''
|
||||
return yaml.round_trip_dump(config, indent=INDENT, block_seq_indent=INDENT)
|
||||
dumper = yaml.YAML()
|
||||
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
||||
rendered = io.StringIO()
|
||||
dumper.dump(config, rendered)
|
||||
|
||||
return rendered.getvalue()
|
||||
|
||||
|
||||
def write_configuration(config_filename, rendered_config, mode=0o600):
|
||||
@@ -112,13 +134,49 @@ def write_configuration(config_filename, rendered_config, mode=0o600):
|
||||
os.chmod(config_filename, mode)
|
||||
|
||||
|
||||
def add_comments_to_configuration(config, schema, indent=0):
|
||||
def add_comments_to_configuration_sequence(config, schema, indent=0):
|
||||
'''
|
||||
If the given config sequence's items are maps, then mine the schema for the description of the
|
||||
map's first item, and slap that atop the sequence. Indent the comment the given number of
|
||||
characters.
|
||||
|
||||
Doing this for sequences of maps results in nice comments that look like:
|
||||
|
||||
```
|
||||
things:
|
||||
# First key description. Added by this function.
|
||||
- key: foo
|
||||
# Second key description. Added by add_comments_to_configuration_map().
|
||||
other: bar
|
||||
```
|
||||
'''
|
||||
if 'map' not in schema['seq'][0]:
|
||||
return
|
||||
|
||||
for field_name in config[0].keys():
|
||||
field_schema = schema['seq'][0]['map'].get(field_name, {})
|
||||
description = field_schema.get('desc')
|
||||
|
||||
# No description to use? Skip it.
|
||||
if not field_schema or not description:
|
||||
return
|
||||
|
||||
config[0].yaml_set_start_comment(description, indent=indent)
|
||||
|
||||
# We only want the first key's description here, as the rest of the keys get commented by
|
||||
# add_comments_to_configuration_map().
|
||||
return
|
||||
|
||||
|
||||
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
|
||||
'''
|
||||
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||
config before each field. This function only adds comments for the top-most config map level.
|
||||
Indent the comment the given number of characters.
|
||||
config mapping, before each field. Indent the comment the given number of characters.
|
||||
'''
|
||||
for index, field_name in enumerate(config.keys()):
|
||||
if skip_first and index == 0:
|
||||
continue
|
||||
|
||||
field_schema = schema['map'].get(field_name, {})
|
||||
description = field_schema.get('desc')
|
||||
|
||||
@@ -127,6 +185,7 @@ def add_comments_to_configuration(config, schema, indent=0):
|
||||
continue
|
||||
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
||||
|
||||
if index > 0:
|
||||
_insert_newline_before_comment(config, field_name)
|
||||
|
||||
|
||||
@@ -126,6 +126,17 @@ map:
|
||||
Exclude directories that contain a file with the given filename. Defaults to not
|
||||
set.
|
||||
example: .nobackup
|
||||
keep_exclude_tags:
|
||||
type: bool
|
||||
desc: |
|
||||
If true, the exclude_if_present filename is included in backups. Defaults to
|
||||
false, meaning that the exclude_if_present filename is omitted from backups.
|
||||
example: true
|
||||
exclude_nodump:
|
||||
type: bool
|
||||
desc: |
|
||||
Exclude files with the NODUMP flag. Defaults to false.
|
||||
example: true
|
||||
storage:
|
||||
desc: |
|
||||
Repository storage options. See
|
||||
@@ -337,8 +348,8 @@ map:
|
||||
example: false
|
||||
hooks:
|
||||
desc: |
|
||||
Shell commands or scripts to execute at various points during a borgmatic run.
|
||||
IMPORTANT: All provided commands and scripts are executed with user permissions of
|
||||
Shell commands, scripts, or integrations to execute at various points during a borgmatic
|
||||
run. IMPORTANT: All provided commands and scripts are executed with user permissions of
|
||||
borgmatic. Do not forget to set secure permissions on this configuration file (chmod
|
||||
0600) as well as on any script called from a hook (chmod 0700) to prevent potential
|
||||
shell injection or privilege escalation.
|
||||
@@ -363,10 +374,80 @@ map:
|
||||
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.
|
||||
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.
|
||||
example:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
postgresql_databases:
|
||||
seq:
|
||||
- map:
|
||||
name:
|
||||
required: true
|
||||
type: str
|
||||
desc: |
|
||||
Database name (required if using this hook). Or "all" to dump all
|
||||
databases on the host.
|
||||
example: users
|
||||
hostname:
|
||||
type: str
|
||||
desc: |
|
||||
Database hostname to connect to. Defaults to connecting via local
|
||||
Unix socket.
|
||||
example: database.example.org
|
||||
port:
|
||||
type: int
|
||||
desc: Port to connect to. Defaults to 5432.
|
||||
example: 5433
|
||||
username:
|
||||
type: str
|
||||
desc: |
|
||||
Username with which to connect to the database. Defaults to the
|
||||
username of the current user. You probably want to specify the
|
||||
"postgres" superuser here when the database name is "all".
|
||||
example: dbuser
|
||||
password:
|
||||
type: str
|
||||
desc: |
|
||||
Password with which to connect to the database. Omitting a password
|
||||
will only work if PostgreSQL is configured to trust the configured
|
||||
username without a password, or you create a ~/.pgpass file.
|
||||
example: trustsome1
|
||||
format:
|
||||
type: str
|
||||
enum: ['plain', 'custom', 'directory', 'tar']
|
||||
desc: |
|
||||
Database dump output format. One of "plain", "custom", "directory",
|
||||
or "tar". Defaults to "custom" (unlike raw pg_dump). See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
Note that format is ignored when the database name is "all".
|
||||
example: directory
|
||||
options:
|
||||
type: str
|
||||
desc: |
|
||||
Additional pg_dump/pg_dumpall options to pass directly to the dump
|
||||
command, without performing any validation on them. See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
example: --role=someone
|
||||
desc: |
|
||||
List of one or more PostgreSQL databases to dump before creating a backup,
|
||||
run once per configuration file. The database dumps are added to your source
|
||||
directories at runtime, backed up, and then removed afterwards. Requires
|
||||
pg_dump/pg_dumpall/pg_restore commands. See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
healthchecks:
|
||||
type: str
|
||||
desc: |
|
||||
Healthchecks ping URL or UUID to notify when a backup begins, ends, or errors.
|
||||
Create an account at https://healthchecks.io if you'd like to use this service.
|
||||
example:
|
||||
https://hc-ping.com/your-uuid-here
|
||||
cronitor:
|
||||
type: str
|
||||
desc: |
|
||||
Cronitor ping URL to notify when a backup begins, ends, or errors. Create an
|
||||
account at https://cronitor.io if you'd like to use this service.
|
||||
example:
|
||||
https://cronitor.link/d3x0c1
|
||||
before_everything:
|
||||
seq:
|
||||
- type: str
|
||||
|
||||
@@ -64,6 +64,23 @@ def apply_logical_validation(config_filename, parsed_configuration):
|
||||
)
|
||||
|
||||
|
||||
def remove_examples(schema):
|
||||
'''
|
||||
pykwalify gets angry if the example field is not a string. So rather than bend to its will,
|
||||
remove all examples from the given schema before passing the schema to pykwalify.
|
||||
'''
|
||||
if 'map' in schema:
|
||||
for item_name, item_schema in schema['map'].items():
|
||||
item_schema.pop('example', None)
|
||||
remove_examples(item_schema)
|
||||
elif 'seq' in schema:
|
||||
for item_schema in schema['seq']:
|
||||
item_schema.pop('example', None)
|
||||
remove_examples(item_schema)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def parse_configuration(config_filename, schema_filename):
|
||||
'''
|
||||
Given the path to a config filename in YAML format and the path to a schema filename in
|
||||
@@ -84,13 +101,7 @@ def parse_configuration(config_filename, schema_filename):
|
||||
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
|
||||
raise Validation_error(config_filename, (str(error),))
|
||||
|
||||
# pykwalify gets angry if the example field is not a string. So rather than bend to its will,
|
||||
# remove all examples before passing the schema to pykwalify.
|
||||
for section_name, section_schema in schema['map'].items():
|
||||
for field_name, field_schema in section_schema['map'].items():
|
||||
field_schema.pop('example', None)
|
||||
|
||||
validator = pykwalify.core.Core(source_data=config, schema_data=schema)
|
||||
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
|
||||
parsed_result = validator.validate(raise_exception=False)
|
||||
|
||||
if validator.validation_errors:
|
||||
|
||||
+51
-14
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -8,10 +9,28 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
||||
BORG_ERROR_EXIT_CODE = 2
|
||||
|
||||
|
||||
def execute_and_log_output(full_command, output_log_level, shell):
|
||||
def exit_code_indicates_error(command, exit_code, error_on_warnings=False):
|
||||
'''
|
||||
Return True if the given exit code from running the command corresponds to an error.
|
||||
'''
|
||||
# If we're running something other than Borg, treat all non-zero exit codes as errors.
|
||||
if 'borg' in command[0] and not error_on_warnings:
|
||||
return bool(exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
|
||||
return bool(exit_code != 0)
|
||||
|
||||
|
||||
def execute_and_log_output(
|
||||
full_command, output_log_level, shell, environment, working_directory, error_on_warnings
|
||||
):
|
||||
last_lines = []
|
||||
process = subprocess.Popen(
|
||||
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
|
||||
full_command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
shell=shell,
|
||||
env=environment,
|
||||
cwd=working_directory,
|
||||
)
|
||||
|
||||
while process.poll() is None:
|
||||
@@ -33,11 +52,7 @@ def execute_and_log_output(full_command, output_log_level, shell):
|
||||
|
||||
exit_code = process.poll()
|
||||
|
||||
# If shell is True, assume we're running something other than Borg and should treat all non-zero
|
||||
# exit codes as errors.
|
||||
error = bool(exit_code != 0) if shell else bool(exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
|
||||
if error:
|
||||
if exit_code_indicates_error(full_command, exit_code, error_on_warnings):
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
# inadvertently hide error output.
|
||||
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
@@ -48,33 +63,55 @@ def execute_and_log_output(full_command, output_log_level, shell):
|
||||
)
|
||||
|
||||
|
||||
def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
||||
def execute_command(
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||
given log level. If output log level is None, instead capture and return the output. If
|
||||
shell is True, execute the command within a shell.
|
||||
shell is True, execute the command within a shell. If an extra environment dict is given, then
|
||||
use it to augment the current environment, and pass the result into the command. If a working
|
||||
directory is given, use that as the present working directory when running the command.
|
||||
|
||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
environment = {**os.environ, **extra_environment} if extra_environment else None
|
||||
|
||||
if output_log_level is None:
|
||||
output = subprocess.check_output(full_command, shell=shell)
|
||||
output = subprocess.check_output(
|
||||
full_command, shell=shell, env=environment, cwd=working_directory
|
||||
)
|
||||
return output.decode() if output is not None else None
|
||||
else:
|
||||
execute_and_log_output(full_command, output_log_level, shell=shell)
|
||||
execute_and_log_output(
|
||||
full_command,
|
||||
output_log_level,
|
||||
shell=shell,
|
||||
environment=environment,
|
||||
working_directory=working_directory,
|
||||
error_on_warnings=error_on_warnings,
|
||||
)
|
||||
|
||||
|
||||
def execute_command_without_capture(full_command):
|
||||
def execute_command_without_capture(full_command, working_directory=None, error_on_warnings=False):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings), but don't capture or log its
|
||||
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
|
||||
display) or provide interactive prompts.
|
||||
|
||||
If a working directory is given, use that as the present working directory when running the
|
||||
command.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
|
||||
try:
|
||||
subprocess.check_call(full_command)
|
||||
subprocess.check_call(full_command, cwd=working_directory)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
||||
if exit_code_indicates_error(full_command, error.returncode, error_on_warnings):
|
||||
raise
|
||||
|
||||
@@ -6,12 +6,26 @@ from borgmatic import execute
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def execute_hook(commands, umask, config_filename, description, dry_run):
|
||||
def interpolate_context(command, context):
|
||||
'''
|
||||
Given a single hook command and a dict of context names/values, interpolate the values by
|
||||
"{name}" into the command and return the result.
|
||||
'''
|
||||
for name, value in context.items():
|
||||
command = command.replace('{%s}' % name, str(value))
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
|
||||
'''
|
||||
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
|
||||
a hook description, and whether this is a dry run, run the given commands. Or, don't run them
|
||||
if this is a dry run.
|
||||
|
||||
The context contains optional values interpolated by name into the hook commands. Currently,
|
||||
this only applies to the on_error hook.
|
||||
|
||||
Raise ValueError if the umask cannot be parsed.
|
||||
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
||||
'''
|
||||
@@ -21,6 +35,9 @@ def execute_hook(commands, umask, config_filename, description, dry_run):
|
||||
|
||||
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
||||
|
||||
context['configuration_filename'] = config_filename
|
||||
commands = [interpolate_context(command, context) for command in commands]
|
||||
|
||||
if len(commands) == 1:
|
||||
logger.info(
|
||||
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
|
||||
@@ -0,0 +1,24 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ping_cronitor(ping_url, config_filename, dry_run, append):
|
||||
'''
|
||||
Ping the given Cronitor URL, appending the append string. Use the given configuration filename
|
||||
in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
if not ping_url:
|
||||
logger.debug('{}: No Cronitor hook set'.format(config_filename))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
ping_url = '{}/{}'.format(ping_url, append)
|
||||
|
||||
logger.info('{}: Pinging Cronitor {}{}'.format(config_filename, append, dry_run_label))
|
||||
logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
requests.get(ping_url)
|
||||
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||
'''
|
||||
Ping the given Healthchecks URL or UUID, appending the append string if any. Use the given
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||
anything.
|
||||
'''
|
||||
if not ping_url_or_uuid:
|
||||
logger.debug('{}: No Healthchecks hook set'.format(config_filename))
|
||||
return
|
||||
|
||||
ping_url = (
|
||||
ping_url_or_uuid
|
||||
if ping_url_or_uuid.startswith('http')
|
||||
else 'https://hc-ping.com/{}'.format(ping_url_or_uuid)
|
||||
)
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
||||
if append:
|
||||
ping_url = '{}/{}'.format(ping_url, append)
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging Healthchecks{}{}'.format(
|
||||
config_filename, ' ' + append if append else '', dry_run_label
|
||||
)
|
||||
)
|
||||
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
requests.get(ping_url)
|
||||
@@ -0,0 +1,184 @@
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
DUMP_PATH = '~/.borgmatic/postgresql_databases'
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_database_dump_filename(name, hostname=None):
|
||||
'''
|
||||
Based on the given database name and hostname, return a filename to use for the database dump.
|
||||
|
||||
Raise ValueError if the database name is invalid.
|
||||
'''
|
||||
if os.path.sep in name:
|
||||
raise ValueError('Invalid database name {}'.format(name))
|
||||
|
||||
return os.path.join(os.path.expanduser(DUMP_PATH), hostname or 'localhost', name)
|
||||
|
||||
|
||||
def dump_databases(databases, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given PostgreSQL databases to disk. The databases are supplied as a sequence of dicts,
|
||||
one dict describing each database as per the configuration schema. Use the given log prefix in
|
||||
any log entries. If this is a dry run, then don't actually dump anything.
|
||||
'''
|
||||
if not databases:
|
||||
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
|
||||
logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
|
||||
|
||||
for database in databases:
|
||||
name = database['name']
|
||||
dump_filename = make_database_dump_filename(name, database.get('hostname'))
|
||||
all_databases = bool(name == 'all')
|
||||
command = (
|
||||
('pg_dumpall' if all_databases else 'pg_dump', '--no-password', '--clean')
|
||||
+ ('--file', dump_filename)
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--username', database['username']) if 'username' in database else ())
|
||||
+ (() if all_databases else ('--format', database.get('format', 'custom')))
|
||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||
+ (() if all_databases else (name,))
|
||||
)
|
||||
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
|
||||
|
||||
logger.debug('{}: Dumping PostgreSQL database {}{}'.format(log_prefix, name, dry_run_label))
|
||||
if not dry_run:
|
||||
os.makedirs(os.path.dirname(dump_filename), mode=0o700, exist_ok=True)
|
||||
execute_command(command, extra_environment=extra_environment)
|
||||
|
||||
|
||||
def remove_database_dumps(databases, log_prefix, dry_run):
|
||||
'''
|
||||
Remove the database dumps for the given databases. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the log prefix in
|
||||
any log entries. If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
if not databases:
|
||||
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
logger.info('{}: Removing PostgreSQL database dumps{}'.format(log_prefix, dry_run_label))
|
||||
|
||||
for database in databases:
|
||||
dump_filename = make_database_dump_filename(database['name'], database.get('hostname'))
|
||||
|
||||
logger.debug(
|
||||
'{}: Removing PostgreSQL database dump {} from {}{}'.format(
|
||||
log_prefix, database['name'], dump_filename, dry_run_label
|
||||
)
|
||||
)
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
os.remove(dump_filename)
|
||||
dump_path = os.path.dirname(dump_filename)
|
||||
|
||||
if len(os.listdir(dump_path)) == 0:
|
||||
os.rmdir(dump_path)
|
||||
|
||||
|
||||
def make_database_dump_patterns(names):
|
||||
'''
|
||||
Given a sequence of database names, return the corresponding glob patterns to match the database
|
||||
dumps in an archive. An empty sequence of names indicates that the patterns should match all
|
||||
dumps.
|
||||
'''
|
||||
return [make_database_dump_filename(name, hostname='*') for name in (names or ['*'])]
|
||||
|
||||
|
||||
def convert_glob_patterns_to_borg_patterns(patterns):
|
||||
'''
|
||||
Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
|
||||
patterns like "sh:etc/*".
|
||||
'''
|
||||
return ['sh:{}'.format(pattern.lstrip(os.path.sep)) for pattern in patterns]
|
||||
|
||||
|
||||
def get_database_names_from_dumps(patterns):
|
||||
'''
|
||||
Given a sequence of database dump patterns, find the corresponding database dumps on disk and
|
||||
return the database names from their filenames.
|
||||
'''
|
||||
return [os.path.basename(dump_path) for pattern in patterns for dump_path in glob.glob(pattern)]
|
||||
|
||||
|
||||
def get_database_configurations(databases, names):
|
||||
'''
|
||||
Given the full database configuration dicts as per the configuration schema, and a sequence of
|
||||
database names, filter down and yield the configuration for just the named databases.
|
||||
Additionally, if a database configuration is named "all", project out that configuration for
|
||||
each named database.
|
||||
|
||||
Raise ValueError if one of the database names cannot be matched to a database in borgmatic's
|
||||
database configuration.
|
||||
'''
|
||||
named_databases = {database['name']: database for database in databases}
|
||||
|
||||
for name in names:
|
||||
database = named_databases.get(name)
|
||||
if database:
|
||||
yield database
|
||||
continue
|
||||
|
||||
if 'all' in named_databases:
|
||||
yield {**named_databases['all'], **{'name': name}}
|
||||
continue
|
||||
|
||||
raise ValueError(
|
||||
'Cannot restore database "{}", as it is not defined in borgmatic\'s configuration'.format(
|
||||
name
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def restore_database_dumps(databases, log_prefix, dry_run):
|
||||
'''
|
||||
Restore the given PostgreSQL databases from disk. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given log
|
||||
prefix in any log entries. If this is a dry run, then don't actually restore anything.
|
||||
'''
|
||||
if not databases:
|
||||
logger.debug('{}: No PostgreSQL databases configured'.format(log_prefix))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
|
||||
for database in databases:
|
||||
dump_filename = make_database_dump_filename(database['name'], database.get('hostname'))
|
||||
restore_command = (
|
||||
('pg_restore', '--no-password', '--clean', '--if-exists', '--exit-on-error')
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--username', database['username']) if 'username' in database else ())
|
||||
+ ('--dbname', database['name'])
|
||||
+ (dump_filename,)
|
||||
)
|
||||
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
|
||||
analyze_command = (
|
||||
('psql', '--no-password', '--quiet')
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--username', database['username']) if 'username' in database else ())
|
||||
+ ('--dbname', database['name'])
|
||||
+ ('--command', 'ANALYZE')
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
'{}: Restoring PostgreSQL database {}{}'.format(
|
||||
log_prefix, database['name'], dry_run_label
|
||||
)
|
||||
)
|
||||
if not dry_run:
|
||||
execute_command(restore_command, extra_environment=extra_environment)
|
||||
execute_command(analyze_command, extra_environment=extra_environment)
|
||||
+22
-7
@@ -73,29 +73,44 @@ def color_text(color, message):
|
||||
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
|
||||
|
||||
|
||||
def configure_logging(console_log_level, syslog_log_level=None):
|
||||
def configure_logging(
|
||||
console_log_level, syslog_log_level=None, log_file_log_level=None, log_file=None
|
||||
):
|
||||
'''
|
||||
Configure logging to go to both the console and syslog. Use the given log levels, respectively.
|
||||
Configure logging to go to both the console and (syslog or log file). Use the given log levels,
|
||||
respectively.
|
||||
|
||||
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
|
||||
'''
|
||||
if syslog_log_level is None:
|
||||
syslog_log_level = console_log_level
|
||||
if log_file_log_level is None:
|
||||
log_file_log_level = console_log_level
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(Console_color_formatter())
|
||||
console_handler.setLevel(console_log_level)
|
||||
|
||||
syslog_path = None
|
||||
if os.path.exists('/dev/log'):
|
||||
syslog_path = '/dev/log'
|
||||
elif os.path.exists('/var/run/syslog'):
|
||||
syslog_path = '/var/run/syslog'
|
||||
if log_file is None:
|
||||
if os.path.exists('/dev/log'):
|
||||
syslog_path = '/dev/log'
|
||||
elif os.path.exists('/var/run/syslog'):
|
||||
syslog_path = '/var/run/syslog'
|
||||
|
||||
if syslog_path and not interactive_console():
|
||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
|
||||
syslog_handler.setLevel(syslog_log_level)
|
||||
handlers = (console_handler, syslog_handler)
|
||||
elif log_file:
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s'))
|
||||
file_handler.setLevel(log_file_log_level)
|
||||
handlers = (console_handler, file_handler)
|
||||
else:
|
||||
handlers = (console_handler,)
|
||||
|
||||
logging.basicConfig(level=min(console_log_level, syslog_log_level), handlers=handlers)
|
||||
logging.basicConfig(
|
||||
level=min(console_log_level, syslog_log_level, log_file_log_level), handlers=handlers
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
|
||||
VERBOSITY_ERROR = -1
|
||||
VERBOSITY_WARNING = 0
|
||||
VERBOSITY_SOME = 1
|
||||
VERBOSITY_LOTS = 2
|
||||
@@ -10,6 +11,7 @@ def verbosity_to_log_level(verbosity):
|
||||
Given a borgmatic verbosity value, return the corresponding Python log level.
|
||||
'''
|
||||
return {
|
||||
VERBOSITY_ERROR: logging.ERROR,
|
||||
VERBOSITY_WARNING: logging.WARNING,
|
||||
VERBOSITY_SOME: logging.INFO,
|
||||
VERBOSITY_LOTS: logging.DEBUG,
|
||||
|
||||
+4
-2
@@ -3,12 +3,14 @@ FROM python:3.7.4-alpine3.10 as borgmatic
|
||||
COPY . /app
|
||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
RUN borgmatic --help > /command-line.txt \
|
||||
&& for action in init prune create check extract list info; do \
|
||||
&& for action in init prune create check extract restore list info; do \
|
||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||
|
||||
FROM node:12.10.0-alpine as html
|
||||
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
RUN npm install @11ty/eleventy \
|
||||
@@ -20,7 +22,7 @@ RUN npm install @11ty/eleventy \
|
||||
COPY --from=borgmatic /etc/borgmatic/config.yaml /source/docs/_includes/borgmatic/config.yaml
|
||||
COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
|
||||
COPY . /source
|
||||
RUN npx eleventy --input=/source/docs --output=/output/docs \
|
||||
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
|
||||
&& mv /output/docs/index.html /output/index.html
|
||||
|
||||
FROM nginx:1.16.1-alpine
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#suggestion-form textarea {
|
||||
font-family: sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#suggestion-form label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#suggestion-form input[type=email] {
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#suggestion-form .form-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<h2>Improve this documentation</h2>
|
||||
|
||||
<p>Have an idea on how to make this documentation even better? Send your
|
||||
feedback below! (But if you need help installing or using borgmatic, please
|
||||
use our <a href="https://torsion.org/borgmatic/#issues">issue tracker</a>
|
||||
instead.)</p>
|
||||
|
||||
<form id="suggestion-form">
|
||||
<div><label for="suggestion">Suggestion</label></div>
|
||||
<textarea id="suggestion" rows="8" cols="60" name="suggestion"></textarea>
|
||||
<div data-sk-error="suggestion" class="form-error"></div>
|
||||
<input id="_page" type="hidden" name="_page">
|
||||
<input id="_subject" type="hidden" name="_subject" value="borgmatic documentation suggestion">
|
||||
<br />
|
||||
<label for="email">Email address</label>
|
||||
<div><input id="email" type="email" name="email" placeholder="Only required if you want a response!"></div>
|
||||
<div data-sk-error="email" class="form-error"></div>
|
||||
<br />
|
||||
<div><button type="submit">Send</button></div>
|
||||
<br />
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('_page').value = window.location.href;
|
||||
window.sk=window.sk||function(){(sk.q=sk.q||[]).push(arguments)};
|
||||
|
||||
sk('form', 'init', {
|
||||
id: '1d536680ab96',
|
||||
element: '#suggestion-form'
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer src="https://js.statickit.com/statickit.js"></script>
|
||||
@@ -1,3 +1,4 @@
|
||||
<header class="elv-layout elv-layout-full elv-header{% if headerClass %} {{ headerClass }}{% endif %}">
|
||||
<h1 class="elv-hed">{{ title | safe }}</h1>
|
||||
{% if page.url != '/' %}<h3><a href="https://torsion.org/borgmatic/">borgmatic</a></h3>{% endif %}
|
||||
<h1 class="elv-hed">{{ title | safe }}</h1>
|
||||
</header>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
{% include 'components/minilink.css' %}
|
||||
{% include 'components/toc.css' %}
|
||||
{% include 'components/info-blocks.css' %}
|
||||
{% include 'components/suggestion-form.css' %}
|
||||
{% include 'prism-theme.css' %}
|
||||
{% include 'asciinema.css' %}
|
||||
{% endset %}
|
||||
|
||||
@@ -8,5 +8,7 @@ headerClass: elv-header-default
|
||||
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
|
||||
<article>
|
||||
{{ content | safe }}
|
||||
|
||||
{% include 'components/suggestion-form.html' %}
|
||||
</article>
|
||||
</main>
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
---
|
||||
title: Add preparation and cleanup steps to backups
|
||||
title: How to add preparation and cleanup steps to backups
|
||||
---
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If you find yourself performing prepraration tasks before your backup runs, or
|
||||
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are
|
||||
shell commands that borgmatic executes for you at various points, and they're
|
||||
configured in the `hooks` section of your configuration file.
|
||||
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are shell
|
||||
commands that borgmatic executes for you at various points, and they're
|
||||
configured in the `hooks` section of your configuration file. But if you're
|
||||
looking to backup a database, it's probably easier to use the [database backup
|
||||
feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
instead.
|
||||
|
||||
For instance, you can specify `before_backup` hooks to dump a database to file
|
||||
before backing it up, and specify `after_backup` hooks to delete the temporary
|
||||
file afterwards. Here's an example:
|
||||
You can specify `before_backup` hooks to perform preparation steps before
|
||||
running backups, and specify `after_backup` hooks to perform cleanup steps
|
||||
afterwards. Here's an example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
before_backup:
|
||||
- dump-a-database /to/file.sql
|
||||
- mount /some/filesystem
|
||||
after_backup:
|
||||
- rm /to/file.sql
|
||||
- umount /some/filesystem
|
||||
```
|
||||
|
||||
The `before_backup` and `after_backup` hooks each run once per configuration
|
||||
@@ -47,25 +50,16 @@ but only if there is a `create` action. It runs even if an error occurs during
|
||||
a backup or a backup hook, but not if an error occurs during a
|
||||
`before_everything` hook.
|
||||
|
||||
## Error hooks
|
||||
|
||||
borgmatic also runs `on_error` hooks if an error occurs, either when creating
|
||||
a backup or running a backup hook. Here's an example configuration:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
```
|
||||
|
||||
Note however that borgmatic does not run `on_error` hooks if an error occurs
|
||||
within a `before_everything` or `after_everything` hook.
|
||||
a backup or running a backup hook. See the [monitoring and alerting
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
for more information.
|
||||
|
||||
## Hook output
|
||||
|
||||
Any output produced by your hooks shows up both at the console and in syslog
|
||||
(when run in a non-interactive console). For more information, read about <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md">inspecting
|
||||
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/">inspecting
|
||||
your backups</a>.
|
||||
|
||||
## Security
|
||||
@@ -79,6 +73,7 @@ invoked by hooks.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
title: How to backup your databases
|
||||
---
|
||||
## Database dump hooks
|
||||
|
||||
If you want to backup a database, it's best practice with most database
|
||||
systems to backup an exported database dump, rather than backing up your
|
||||
database's internal file storage. That's because the internal storage can
|
||||
change while you're reading from it. In contrast, a database dump creates a
|
||||
consistent snapshot that is more suited for backups.
|
||||
|
||||
Fortunately, borgmatic includes built-in support for creating database dumps
|
||||
prior to running backups. For example, here is everything you need to dump and
|
||||
backup a couple of local PostgreSQL databases:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
- name: orders
|
||||
```
|
||||
|
||||
Prior to each backup, borgmatic dumps each configured database to a file
|
||||
(located in `~/.borgmatic/`) and includes it in the backup. After the backup
|
||||
completes, borgmatic removes the database dump files to recover disk space.
|
||||
|
||||
Here's a more involved example that connects to a remote database:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: database.example.org
|
||||
port: 5433
|
||||
username: dbuser
|
||||
password: trustsome1
|
||||
format: tar
|
||||
options: "--role=someone"
|
||||
```
|
||||
|
||||
If you want to dump all databases on a host, use `all` for the database name:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: all
|
||||
```
|
||||
|
||||
Note that you may need to use a `username` of the `postgres` superuser for
|
||||
this to work.
|
||||
|
||||
|
||||
### Configuration backups
|
||||
|
||||
An important note about this database configuration: You'll need the
|
||||
configuration to be present in order for borgmatic to restore a database. So
|
||||
to prepare for this situation, it's a good idea to include borgmatic's own
|
||||
configuration files as part of your regular backups. That way, you can always
|
||||
bring back any missing configuration files in order to restore a database.
|
||||
|
||||
|
||||
## Supported databases
|
||||
|
||||
As of now, borgmatic only supports PostgreSQL databases directly. But see
|
||||
below about general-purpose preparation and cleanup hooks as a work-around
|
||||
with other database systems. Also, please [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) for additional database systems
|
||||
that you'd like supported.
|
||||
|
||||
|
||||
## Database restoration
|
||||
|
||||
To restore a database dump from an archive, use the `borgmatic restore`
|
||||
action. But the first step is to figure out which archive to restore from. A
|
||||
good way to do that is to use the `list` action:
|
||||
|
||||
```bash
|
||||
borgmatic list
|
||||
```
|
||||
|
||||
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
That should yield output looking something like:
|
||||
|
||||
```text
|
||||
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...]
|
||||
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...]
|
||||
```
|
||||
|
||||
Assuming that you want to restore all database dumps from the archive with the
|
||||
most up-to-date files and therefore the latest timestamp, run a command like:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive host-2019-01-02T04:06:07.080910
|
||||
```
|
||||
|
||||
(No borgmatic `restore` action? Upgrade borgmatic!)
|
||||
|
||||
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.
|
||||
|
||||
This is a destructive action! `borgmatic restore` replaces live databases by
|
||||
restoring dumps from the selected archive. So be very careful when and where
|
||||
you run it.
|
||||
|
||||
|
||||
### Repository selection
|
||||
|
||||
If you have a single repository in your borgmatic configuration file(s), no
|
||||
problem: the `restore` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository path containing the archive to restore. Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic restore --repository repo.borg --archive host-2019-...
|
||||
```
|
||||
|
||||
### Restore particular databases
|
||||
|
||||
If you've backed up multiple databases into an archive, and you'd only like to
|
||||
restore one of them, use the `--database` flag to select one or more
|
||||
databases. For instance:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive host-2019-... --database users
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
There are a few important limitations with borgmatic's current database
|
||||
restoration feature that you should know about:
|
||||
|
||||
1. You must restore as the same Unix user that created the archive containing
|
||||
the database dump. That's because the user's home directory path is encoded
|
||||
into the path of the database dump within the archive.
|
||||
2. As mentioned above, borgmatic can only restore a database that's defined in
|
||||
borgmatic's own configuration file. So include your configuration file in
|
||||
backups to avoid getting caught without a way to restore a database.
|
||||
3. borgmatic does not currently support backing up or restoring multiple
|
||||
databases that share the exact same name on different hosts.
|
||||
|
||||
|
||||
### Manual restoration
|
||||
|
||||
If you prefer to restore a database without the help of borgmatic, first
|
||||
[extract](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/) an
|
||||
archive containing a database dump, and then manually restore the dump file
|
||||
found within the extracted `~/.borgmatic/` path (e.g. with `pg_restore`).
|
||||
|
||||
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If this database integration is too limited for needs, borgmatic also supports
|
||||
general-purpose [preparation and cleanup
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/).
|
||||
These hooks allows you to trigger arbitrary commands or scripts before and
|
||||
after backups. So if necessary, you can use these hooks to create database
|
||||
dumps with any database system.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
|
||||
@@ -14,7 +14,7 @@ repositories.
|
||||
|
||||
If you find yourself in this situation, you have some options. First, you can
|
||||
run borgmatic's pruning, creating, or checking actions separately. For
|
||||
instance, the the following optional flags are available:
|
||||
instance, the the following optional actions are available:
|
||||
|
||||
```bash
|
||||
borgmatic prune
|
||||
@@ -25,7 +25,7 @@ borgmatic check
|
||||
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
|
||||
`--prune`, `--create`, or `--check`. Or upgrade borgmatic!)
|
||||
|
||||
You can run with only one of these flags provided, or you can mix and match
|
||||
You can run with only one of these actions provided, or you can mix and match
|
||||
any number of them in a single borgmatic run. This supports approaches like
|
||||
making backups with `create` on a frequent schedule, while only running
|
||||
expensive consistency checks with `check` on a much less frequent basis from
|
||||
@@ -106,4 +106,4 @@ backups.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
||||
@@ -62,6 +62,8 @@ following:
|
||||
tox -e black
|
||||
```
|
||||
|
||||
Note that Black requires at minimum Python 3.6.
|
||||
|
||||
And if you get a complaint from the
|
||||
[isort](https://github.com/timothycrosley/isort) Python import orderer, you
|
||||
can ask isort to order your imports for you:
|
||||
@@ -107,6 +109,29 @@ suite. You can view these builds on
|
||||
[build.torsion.org](https://build.torsion.org/witten/borgmatic), and they're
|
||||
also linked from the commits list on each pull request.
|
||||
|
||||
## Documentation development
|
||||
|
||||
Updates to borgmatic's documentation are welcome. It's formatted in Markdown
|
||||
and located in the `docs/` directory in borgmatic's source, plus the
|
||||
`README.md` file at the root.
|
||||
|
||||
To build and view a copy of the documentation with your local changes, run the
|
||||
following from the root of borgmatic's source code:
|
||||
|
||||
```bash
|
||||
sudo scripts/dev-docs
|
||||
```
|
||||
|
||||
This requires Docker to be installed on your system. You may not need to use
|
||||
sudo if your non-root user has permissions to run Docker.
|
||||
|
||||
After you run the script, you can point your web browser at
|
||||
http://localhost:8080 to view the documentation with your changes.
|
||||
|
||||
To close the documentation server, ctrl-C the script. Note that it does not
|
||||
currently auto-reload, so you'll need to stop it and re-run it for any
|
||||
additional documentation changes to take effect.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: How to extract a backup
|
||||
---
|
||||
## Extract
|
||||
|
||||
When the worst happens—or you want to test your backups—the first step is
|
||||
to figure out which archive to extract. A good way to do that is to use the
|
||||
`list` action:
|
||||
|
||||
```bash
|
||||
borgmatic list
|
||||
```
|
||||
|
||||
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
That should yield output looking something like:
|
||||
|
||||
```text
|
||||
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...]
|
||||
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...]
|
||||
```
|
||||
|
||||
Assuming that you want to extract the archive with the most up-to-date files
|
||||
and therefore the latest timestamp, run a command like:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
||||
```
|
||||
|
||||
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Repository selection
|
||||
|
||||
If you have a single repository in your borgmatic configuration file(s), no
|
||||
problem: the `extract` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository path containing the archive to extract. Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic extract --repository repo.borg --archive host-2019-...
|
||||
```
|
||||
|
||||
## Extract particular files
|
||||
|
||||
Sometimes, you want to extract a single deleted file, rather than extracting
|
||||
everything from an archive. To do that, tack on one or more `--path` values.
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive host-2019-... --path path/1 path/2
|
||||
```
|
||||
|
||||
Note that the specified restore paths should not have a leading slash. Like a
|
||||
whole-archive extract, this also extracts into the current directory. So for
|
||||
example, if you happen to be in the directory `/var` and you run the `extract`
|
||||
command above, borgmatic will extract `/var/path/1` and `/var/path/2`.
|
||||
|
||||
## Extract to a particular destination
|
||||
|
||||
By default, borgmatic extracts files into the current directory. To instead
|
||||
extract files to a particular destination directory, use the `--destination`
|
||||
flag:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive host-2019-... --destination /tmp
|
||||
```
|
||||
|
||||
When using the `--destination` flag, be careful not to overwrite your system's
|
||||
files with extracted files unless that is your intent.
|
||||
|
||||
|
||||
## Database restoration
|
||||
|
||||
The `borgmatic extract` command only extracts files. To restore a database,
|
||||
please see the [documentation on database backups and
|
||||
restores](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/).
|
||||
borgmatic does not perform database restoration as part of `borgmatic extract`
|
||||
so that you can extract files from your archive without impacting your live
|
||||
databases.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
@@ -22,9 +22,9 @@ borgmatic --verbosity 2
|
||||
|
||||
## Backup summary
|
||||
|
||||
If you're less concerned with progress during a backup, and you just want to
|
||||
If you're less concerned with progress during a backup, and you only want to
|
||||
see the summary of archive statistics at the end, you can use the stats
|
||||
option:
|
||||
option when performing a backup:
|
||||
|
||||
```bash
|
||||
borgmatic --stats
|
||||
@@ -32,7 +32,7 @@ borgmatic --stats
|
||||
|
||||
## Existing backups
|
||||
|
||||
Borgmatic provides convenient flags for Borg's
|
||||
borgmatic provides convenient actions for Borg's
|
||||
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
|
||||
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
||||
functionality:
|
||||
@@ -46,6 +46,7 @@ borgmatic info
|
||||
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
|
||||
`--info`. Or upgrade borgmatic!)
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
||||
@@ -69,6 +70,20 @@ Or to increase syslog logging to include debug spew:
|
||||
borgmatic --syslog-verbosity 2
|
||||
```
|
||||
|
||||
### Logging to file
|
||||
|
||||
If you don't want to use syslog, and you'd rather borgmatic log to a plain
|
||||
file, use the `--log-file` flag:
|
||||
|
||||
```bash
|
||||
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
|
||||
`--log-file-verbosity` flag to customize the log file's log level.
|
||||
|
||||
|
||||
### systemd journal
|
||||
|
||||
If your local syslog daemon is systemd's journal, be aware that journald by
|
||||
@@ -82,18 +97,10 @@ Note that the [sample borgmatic systemd service
|
||||
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
|
||||
already has this rate limit disabled.
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
optional `--json` flag with `create`, `list`, or `info` to get the output
|
||||
formatted as JSON.
|
||||
|
||||
Note that when you specify the `--json` flag, Borg's other non-JSON output is
|
||||
suppressed so as not to interfere with the captured JSON. Also note that JSON
|
||||
output only shows up at the console, and not in syslog.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
||||
@@ -112,4 +112,4 @@ directly, please see the section above about standard includes.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
title: How to monitor your backups
|
||||
---
|
||||
|
||||
## Monitoring and alerting
|
||||
|
||||
Having backups is great, but they won't do you a lot of good unless you have
|
||||
confidence that they're running on a regular basis. That's where monitoring
|
||||
and alerting comes in.
|
||||
|
||||
There are several different ways you can monitor your backups and find out
|
||||
whether they're succeeding. Which of these you choose to do is up to you and
|
||||
your particular infrastructure:
|
||||
|
||||
1. **Job runner alerts**: The easiest place to start is with failure alerts
|
||||
from the [scheduled job
|
||||
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot) (cron,
|
||||
systemd, etc.) that's running borgmatic. But note that if the job doesn't even
|
||||
get scheduled (e.g. due to the job runner not running), you probably won't get
|
||||
an alert at all! Still, this is a decent first line of defense, especially
|
||||
when combined with some of the other approaches below.
|
||||
2. **borgmatic error hooks**: The `on_error` hook allows you to run an arbitrary
|
||||
command or script when borgmatic itself encounters an error running your
|
||||
backups. So for instance, you can run a script to send yourself a text message
|
||||
alert. But note that if borgmatic doesn't actually run, this alert won't fire.
|
||||
See [error
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
|
||||
below for how to configure this.
|
||||
4. **borgmatic monitoring hooks**: This feature integrates with monitoring
|
||||
services like [Healthchecks](https://healthchecks.io/) and
|
||||
[Cronitor](https://cronitor.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
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
and [Cronitor
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-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
|
||||
successful backup occurred. See [scripting
|
||||
borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#scripting-borgmatic)
|
||||
below for how to configure this.
|
||||
5. **Borg hosting providers**: Most [Borg hosting
|
||||
providers](https://torsion.org/borgmatic/#hosting-providers) include
|
||||
monitoring and alerting as part of their offering. This gives you a dashboard
|
||||
to check on all of your backups, and can alert you if the service doesn't hear
|
||||
from borgmatic for a configured interval.
|
||||
6. **borgmatic consistency checks**: While not strictly part of monitoring, if you
|
||||
really want confidence that your backups are not only running but are
|
||||
restorable as well, you can configure particular [consistency
|
||||
checks](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration)
|
||||
or even script full [extract
|
||||
tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
|
||||
|
||||
|
||||
## Error hooks
|
||||
|
||||
When an error occurs during a backup, borgmatic can run configurable shell
|
||||
commands to fire off custom error notifications or take other actions, so you
|
||||
can get alerted as soon as something goes wrong. Here's a not-so-useful
|
||||
example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
```
|
||||
|
||||
The `on_error` hook supports interpolating particular runtime variables into
|
||||
the hook command. Here's an example that assumes you provide a separate shell
|
||||
script to handle the alerting:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- send-text-message.sh "{configuration_filename}" "{repository}"
|
||||
```
|
||||
|
||||
In this example, when the error occurs, borgmatic interpolates a few runtime
|
||||
values into the hook command: the borgmatic configuration filename, and the
|
||||
path of the repository. Here's the full set of supported variables you can use
|
||||
here:
|
||||
|
||||
* `configuration_filename`: borgmatic configuration filename in which the
|
||||
error occurred
|
||||
* `repository`: path of the repository in which the error occurred (may be
|
||||
blank if the error occurs in a hook)
|
||||
* `error`: the error message itself
|
||||
* `output`: output of the command that failed (may be blank if an error
|
||||
occurred without running a command)
|
||||
|
||||
Note that borgmatic does not run `on_error` hooks if an error occurs within a
|
||||
`before_everything` or `after_everything` hook. For more about hooks, see the
|
||||
[borgmatic hooks
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
especially the security information.
|
||||
|
||||
|
||||
## Healthchecks hook
|
||||
|
||||
[Healthchecks](https://healthchecks.io/) is a service that provides "instant
|
||||
alerts when your cron jobs fail silently", and borgmatic has built-in
|
||||
integration with it. Once you create a Healthchecks account and project on
|
||||
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||
URL" for your project. Here's an example:
|
||||
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
healthchecks: https://hc-ping.com/addffa72-da17-40ae-be9c-ff591afb942a
|
||||
```
|
||||
|
||||
With this hook in place, borgmatic will ping your Healthchecks project when a
|
||||
backup begins, ends, or errors. Then you can configure Healthchecks to notify
|
||||
you by a [variety of
|
||||
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
||||
or it doesn't hear from borgmatic for a certain period of time.
|
||||
|
||||
|
||||
## Cronitor hook
|
||||
|
||||
[Cronitor](https://cronitor.io/) provides "Cron monitoring and uptime healthchecks
|
||||
for websites, services and APIs", and borgmatic has built-in
|
||||
integration with it. Once you create a Cronitor account and cron job monitor on
|
||||
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||
API URL" for your monitor. Here's an example:
|
||||
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
cronitor: https://cronitor.link/d3x0c1
|
||||
```
|
||||
|
||||
With this hook in place, borgmatic will ping your Cronitor monitor when a
|
||||
backup begins, ends, or errors. Then you can configure Cronitor to notify you
|
||||
by a [variety of
|
||||
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups
|
||||
fail or it doesn't hear from borgmatic for a certain period of time.
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
optional `--json` flag with `create`, `list`, or `info` to get the output
|
||||
formatted as JSON.
|
||||
|
||||
Note that when you specify the `--json` flag, Borg's other non-JSON output is
|
||||
suppressed so as not to interfere with the captured JSON. Also note that JSON
|
||||
output only shows up at the console, and not in syslog.
|
||||
|
||||
|
||||
### Successful backups
|
||||
|
||||
`borgmatic list` includes support for a `--successful` flag that only lists
|
||||
successful (non-checkpoint) backups. This flag works via a basic heuristic: It
|
||||
assumes that non-checkpoint archive names end with a digit (e.g. from a
|
||||
timestamp), while checkpoint archive names do not. This means that if you're
|
||||
using custom archive names that do not end in a digit, the `--successful` flag
|
||||
will not work as expected.
|
||||
|
||||
Combined with a built-in Borg flag like `--last`, you can list the last
|
||||
successful backup for use in your monitoring scripts. Here's an example
|
||||
combined with `--json`:
|
||||
|
||||
```bash
|
||||
borgmatic list --successful --last 1 --json
|
||||
```
|
||||
|
||||
Note that this particular combination will only work if you've got a single
|
||||
backup "series" in your repository. If you're instead backing up, say, from
|
||||
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.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
@@ -1,67 +1,3 @@
|
||||
---
|
||||
title: How to restore a backup
|
||||
---
|
||||
## Extract
|
||||
|
||||
When the worst happens—or you want to test your backups—the first step is
|
||||
to figure out which archive to restore. A good way to do that is to use the
|
||||
`list` action:
|
||||
|
||||
```bash
|
||||
borgmatic list
|
||||
```
|
||||
|
||||
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
That should yield output looking something like:
|
||||
|
||||
```text
|
||||
host-2019-01-01T04:05:06.070809 Tue, 2019-01-01 04:05:06 [...]
|
||||
host-2019-01-02T04:06:07.080910 Wed, 2019-01-02 04:06:07 [...]
|
||||
```
|
||||
|
||||
Assuming that you want to restore the archive with the most up-to-date files
|
||||
and therefore the latest timestamp, run a command like:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
||||
```
|
||||
|
||||
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
The `--archive` value is the name of the archive to restore. 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.
|
||||
|
||||
|
||||
## Repository selection
|
||||
|
||||
If you have a single repository in your borgmatic configuration file(s), no
|
||||
problem: the `extract` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository path containing the archive to extract. Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic extract --repository repo.borg --archive host-2019-...
|
||||
```
|
||||
|
||||
## Restore particular files
|
||||
|
||||
Sometimes, you want to restore a single deleted file, rather than restoring
|
||||
everything from an archive. To do that, tack on one or more `--restore-path`
|
||||
values. For instance:
|
||||
|
||||
```bash
|
||||
borgmatic extract --archive host-2019-... --restore-path /path/1 /path/2
|
||||
```
|
||||
|
||||
Like a whole-archive restore, this also restores into the current directory.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
<head>
|
||||
<meta http-equiv='refresh' content='0; URL=https://torsion.org/borgmatic/docs/how-to/extract-a-backup/'>
|
||||
</head>
|
||||
|
||||
@@ -35,7 +35,7 @@ borgmatic:
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Arch Linux](https://aur.archlinux.org/packages/borgmatic/)
|
||||
* [Arch Linux](https://www.archlinux.org/packages/community/any/borgmatic/)
|
||||
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [stand-alone binary](https://github.com/cmarquardt/borgmatic-binary)
|
||||
@@ -77,7 +77,7 @@ 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
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration.md), the authoritative set of
|
||||
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.
|
||||
|
||||
@@ -228,7 +228,7 @@ found character that cannot start any token
|
||||
in "config.yaml", line 230, column 1
|
||||
```
|
||||
|
||||
YAML does not allow tabs. So to fix this, simply replace any tabs in your
|
||||
YAML does not allow tabs. So to fix this, replace any tabs in your
|
||||
configuration file with the requisite number of spaces.
|
||||
|
||||
### libyaml compilation errors
|
||||
@@ -244,13 +244,9 @@ it.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups.md)
|
||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration.md)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line.md)
|
||||
|
||||
<script>
|
||||
var links = document.getElementsByClassName("referral");
|
||||
links[Math.floor(Math.random() * links.length)].style.display = "none";
|
||||
</script>
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||
|
||||
@@ -76,4 +76,4 @@ files.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
||||
@@ -13,5 +13,5 @@ each action sub-command:
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||
|
||||
@@ -15,5 +15,5 @@ file](https://torsion.org/borgmatic/docs/reference/config.yaml) for use locally.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||
|
||||
+1
-2
@@ -2,9 +2,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
docker build --tag borgmatic-docs --file docs/Dockerfile .
|
||||
docker build --tag borgmatic-docs --build-arg ENVIRONMENT=dev --file docs/Dockerfile .
|
||||
echo
|
||||
echo "You can view dev docs at http://localhost:8080"
|
||||
echo "Note that links within these docs will go to the online docs, so you will need to fiddle with URLs manually to stay in the dev docs."
|
||||
echo
|
||||
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Temporary work around for https://github.com/pypa/pip/issues/6434
|
||||
python -m pip install --upgrade pip==19.1.1
|
||||
python -m pip install --no-use-pep517 $*
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
python -m pip install --upgrade pip==19.1.1
|
||||
pip install tox==3.10.0
|
||||
python -m pip install --upgrade pip==19.3.1
|
||||
pip install tox==3.14.0
|
||||
tox
|
||||
apk add --no-cache borgbackup
|
||||
tox -e end-to-end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.3.22'
|
||||
VERSION = '1.4.6'
|
||||
|
||||
|
||||
setup(
|
||||
@@ -31,6 +31,7 @@ setup(
|
||||
obsoletes=['atticmatic'],
|
||||
install_requires=(
|
||||
'pykwalify>=1.6.0,<14.06',
|
||||
'requests',
|
||||
'ruamel.yaml>0.15.0,<0.17.0',
|
||||
'setuptools',
|
||||
'colorama>=0.4.1,<0.5',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
attrs==19.3.0
|
||||
black==19.3b0; python_version >= '3.6'
|
||||
click==7.0
|
||||
colorama==0.4.1
|
||||
coverage==4.5.4
|
||||
docopt==0.6.2
|
||||
flake8==3.7.8
|
||||
flake8==3.7.9
|
||||
flexmock==0.10.4
|
||||
isort==4.3.21
|
||||
mccabe==0.6.1
|
||||
@@ -16,9 +16,10 @@ py==1.8.0
|
||||
pycodestyle==2.5.0
|
||||
pyflakes==2.1.1
|
||||
pykwalify==1.7.0
|
||||
pytest==5.1.2
|
||||
pytest-cov==2.7.1
|
||||
pytest==5.2.2
|
||||
pytest-cov==2.8.1
|
||||
python-dateutil==2.8.0
|
||||
PyYAML==5.1.2
|
||||
requests==2.22.0
|
||||
ruamel.yaml>0.15.0,<0.17.0
|
||||
toml==0.10.0
|
||||
|
||||
@@ -15,6 +15,7 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||
assert global_arguments.excludes_filename is None
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
|
||||
|
||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||
@@ -26,6 +27,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
|
||||
|
||||
def test_parse_arguments_with_verbosity_overrides_default():
|
||||
@@ -39,6 +41,7 @@ def test_parse_arguments_with_verbosity_overrides_default():
|
||||
assert global_arguments.excludes_filename is None
|
||||
assert global_arguments.verbosity == 1
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
|
||||
|
||||
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||
@@ -54,6 +57,20 @@ def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||
assert global_arguments.syslog_verbosity == 2
|
||||
|
||||
|
||||
def test_parse_arguments_with_log_file_verbosity_overrides_default():
|
||||
config_paths = ['default']
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||
|
||||
arguments = module.parse_arguments('--log-file-verbosity', '-1')
|
||||
|
||||
global_arguments = arguments['global']
|
||||
assert global_arguments.config_paths == config_paths
|
||||
assert global_arguments.excludes_filename is None
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == -1
|
||||
|
||||
|
||||
def test_parse_arguments_with_list_json_overrides_default():
|
||||
arguments = module.parse_arguments('list', '--json')
|
||||
|
||||
@@ -230,6 +247,15 @@ def test_parse_arguments_disallows_init_and_dry_run():
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_glob_archives_with_successful():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments(
|
||||
'--config', 'myconfig', 'list', '--glob-archives', '*glob*', '--successful'
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -251,18 +277,18 @@ def test_parse_arguments_allows_repository_with_list():
|
||||
module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_archive_without_extract_or_list():
|
||||
def test_parse_arguments_disallows_archive_without_extract_restore_or_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_restore_paths_without_extract():
|
||||
def test_parse_arguments_disallows_paths_without_extract():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', '--restore-path', 'test')
|
||||
module.parse_arguments('--config', 'myconfig', '--path', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_extract():
|
||||
@@ -277,6 +303,18 @@ def test_parse_arguments_allows_archive_with_dashed_extract():
|
||||
module.parse_arguments('--config', 'myconfig', '--extract', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_restore():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--config', 'myconfig', 'restore', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_dashed_restore():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--config', 'myconfig', '--restore', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -290,6 +328,13 @@ def test_parse_arguments_requires_archive_with_extract():
|
||||
module.parse_arguments('--config', 'myconfig', 'extract')
|
||||
|
||||
|
||||
def test_parse_arguments_requires_archive_with_restore():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', 'restore')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_progress_before_create():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -308,6 +353,12 @@ def test_parse_arguments_allows_progress_and_extract():
|
||||
module.parse_arguments('--progress', 'extract', '--archive', 'test', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_progress_and_restore():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--progress', 'restore', '--archive', 'test', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_progress_without_create():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ def test_comment_out_line_comments_indented_option():
|
||||
assert module._comment_out_line(line) == ' # enabled: true'
|
||||
|
||||
|
||||
def test_comment_out_line_comments_twice_indented_option():
|
||||
line = ' - item'
|
||||
|
||||
assert module._comment_out_line(line) == ' # - item'
|
||||
|
||||
|
||||
def test_comment_out_optional_configuration_comments_optional_config_only():
|
||||
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
||||
config = '''
|
||||
@@ -74,10 +80,10 @@ location:
|
||||
assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip()
|
||||
|
||||
|
||||
def test_render_configuration_does_not_raise():
|
||||
flexmock(module.yaml).should_receive('round_trip_dump')
|
||||
def test_render_configuration_converts_configuration_to_yaml_string():
|
||||
yaml_string = module._render_configuration({'foo': 'bar'})
|
||||
|
||||
module._render_configuration({})
|
||||
assert yaml_string == 'foo: bar\n'
|
||||
|
||||
|
||||
def test_write_configuration_does_not_raise():
|
||||
@@ -107,12 +113,33 @@ def test_write_configuration_with_already_existing_directory_does_not_raise():
|
||||
module.write_configuration('config.yaml', 'config: yaml')
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_does_not_raise():
|
||||
def test_add_comments_to_configuration_sequence_of_strings_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq(['foo', 'bar'])
|
||||
schema = {'seq': [{'type': 'str'}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_sequence_of_maps_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
|
||||
schema = {'seq': [{'map': {'foo': {'desc': 'yo'}}}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_sequence_of_maps_without_description_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
|
||||
schema = {'seq': [{'map': {'foo': {}}}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_map_does_not_raise():
|
||||
# Ensure that it can deal with fields both in the schema and missing from the schema.
|
||||
config = module.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {'map': {'foo': {'desc': 'Foo'}, 'bar': {'desc': 'Bar'}}}
|
||||
|
||||
module.add_comments_to_configuration(config, schema)
|
||||
module.add_comments_to_configuration_map(config, schema)
|
||||
|
||||
|
||||
def test_generate_sample_configuration_does_not_raise():
|
||||
|
||||
@@ -10,43 +10,58 @@ from borgmatic import execute as module
|
||||
def test_execute_and_log_output_logs_each_line_separately():
|
||||
flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').once()
|
||||
flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once()
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(False)
|
||||
|
||||
module.execute_and_log_output(['echo', 'hi'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(['echo', 'there'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['echo', 'hi'],
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
module.execute_and_log_output(
|
||||
['echo', 'there'],
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_borg_warning_does_not_raise():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
|
||||
# Borg's exit code 1 is a warning, not an error.
|
||||
module.execute_and_log_output(['false'], output_log_level=logging.INFO, shell=False)
|
||||
|
||||
|
||||
def test_execute_and_log_output_includes_borg_error_output_in_exception():
|
||||
def test_execute_and_log_output_includes_error_output_in_exception():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(['grep'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['grep'],
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
assert error.value.returncode == 2
|
||||
assert error.value.output
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_shell_error_raises():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(['false'], output_log_level=logging.INFO, shell=True)
|
||||
|
||||
assert error.value.returncode == 1
|
||||
|
||||
|
||||
def test_execute_and_log_output_truncates_long_borg_error_output():
|
||||
def test_execute_and_log_output_truncates_long_error_output():
|
||||
flexmock(module).ERROR_OUTPUT_MAX_LINE_COUNT = 0
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(['grep'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['grep'],
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
assert error.value.returncode == 2
|
||||
assert error.value.output.startswith('...')
|
||||
@@ -54,12 +69,13 @@ def test_execute_and_log_output_truncates_long_borg_error_output():
|
||||
|
||||
def test_execute_and_log_output_with_no_output_logs_nothing():
|
||||
flexmock(module.logger).should_receive('log').never()
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(False)
|
||||
|
||||
module.execute_and_log_output(['true'], output_log_level=logging.INFO, shell=False)
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_error_exit_status_raises():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
module.execute_and_log_output(['grep'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['true'],
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
@@ -150,17 +150,56 @@ def test_make_exclude_flags_includes_exclude_if_present_when_in_config():
|
||||
assert exclude_flags == ('--exclude-if-present', 'exclude_me')
|
||||
|
||||
|
||||
def test_make_exclude_flags_includes_keep_exclude_tags_when_true_in_config():
|
||||
exclude_flags = module._make_exclude_flags(location_config={'keep_exclude_tags': True})
|
||||
|
||||
assert exclude_flags == ('--keep-exclude-tags',)
|
||||
|
||||
|
||||
def test_make_exclude_flags_does_not_include_keep_exclude_tags_when_false_in_config():
|
||||
exclude_flags = module._make_exclude_flags(location_config={'keep_exclude_tags': False})
|
||||
|
||||
assert exclude_flags == ()
|
||||
|
||||
|
||||
def test_make_exclude_flags_includes_exclude_nodump_when_true_in_config():
|
||||
exclude_flags = module._make_exclude_flags(location_config={'exclude_nodump': True})
|
||||
|
||||
assert exclude_flags == ('--exclude-nodump',)
|
||||
|
||||
|
||||
def test_make_exclude_flags_does_not_include_exclude_nodump_when_false_in_config():
|
||||
exclude_flags = module._make_exclude_flags(location_config={'exclude_nodump': False})
|
||||
|
||||
assert exclude_flags == ()
|
||||
|
||||
|
||||
def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
|
||||
exclude_flags = module._make_exclude_flags(location_config={})
|
||||
|
||||
assert exclude_flags == ()
|
||||
|
||||
|
||||
def test_borgmatic_source_directories_set_when_directory_exists():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os.path).should_receive('expanduser')
|
||||
|
||||
assert module.borgmatic_source_directories() == [module.BORGMATIC_SOURCE_DIRECTORY]
|
||||
|
||||
|
||||
def test_borgmatic_source_directories_empty_when_directory_does_not_exist():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os.path).should_receive('expanduser')
|
||||
|
||||
assert module.borgmatic_source_directories() == []
|
||||
|
||||
|
||||
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
|
||||
ARCHIVE_WITH_PATHS = ('repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar')
|
||||
|
||||
|
||||
def test_create_archive_calls_borg_with_parameters():
|
||||
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)
|
||||
@@ -184,6 +223,7 @@ def test_create_archive_calls_borg_with_parameters():
|
||||
|
||||
def test_create_archive_with_patterns_calls_borg_with_patterns():
|
||||
pattern_flags = ('--patterns-from', 'patterns')
|
||||
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(
|
||||
@@ -209,6 +249,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
|
||||
|
||||
def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||
exclude_flags = ('--exclude-from', 'excludes')
|
||||
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(('exclude',))
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None).and_return(
|
||||
@@ -233,6 +274,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||
|
||||
|
||||
def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
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)
|
||||
@@ -258,6 +300,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
|
||||
|
||||
def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||
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)
|
||||
@@ -283,6 +326,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||
|
||||
|
||||
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
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)
|
||||
@@ -308,6 +352,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
|
||||
|
||||
def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
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)
|
||||
@@ -332,6 +377,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
|
||||
|
||||
def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||
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)
|
||||
@@ -357,6 +403,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():
|
||||
# --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)
|
||||
@@ -385,6 +432,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
||||
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)
|
||||
@@ -411,6 +459,7 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
|
||||
|
||||
|
||||
def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters():
|
||||
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)
|
||||
@@ -434,6 +483,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
||||
|
||||
|
||||
def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
|
||||
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)
|
||||
@@ -457,6 +507,7 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
|
||||
|
||||
|
||||
def test_create_archive_with_compression_calls_borg_with_compression_parameters():
|
||||
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)
|
||||
@@ -480,6 +531,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
|
||||
|
||||
|
||||
def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_parameters():
|
||||
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)
|
||||
@@ -503,6 +555,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_
|
||||
|
||||
|
||||
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter():
|
||||
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)
|
||||
@@ -526,6 +579,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
|
||||
|
||||
|
||||
def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_parameter():
|
||||
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)
|
||||
@@ -549,6 +603,7 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_paramet
|
||||
|
||||
|
||||
def test_create_archive_with_read_special_calls_borg_with_read_special_parameter():
|
||||
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)
|
||||
@@ -573,6 +628,7 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
||||
|
||||
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||
def test_create_archive_with_option_true_calls_borg_without_corresponding_parameter(option_name):
|
||||
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)
|
||||
@@ -597,6 +653,7 @@ def test_create_archive_with_option_true_calls_borg_without_corresponding_parame
|
||||
|
||||
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||
def test_create_archive_with_option_false_calls_borg_with_corresponding_parameter(option_name):
|
||||
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)
|
||||
@@ -621,6 +678,7 @@ def test_create_archive_with_option_false_calls_borg_with_corresponding_paramete
|
||||
|
||||
|
||||
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
|
||||
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)
|
||||
@@ -645,6 +703,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
|
||||
|
||||
|
||||
def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||
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)
|
||||
@@ -668,6 +727,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||
|
||||
|
||||
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
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)
|
||||
@@ -692,6 +752,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
|
||||
|
||||
def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||
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)
|
||||
@@ -714,6 +775,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||
|
||||
|
||||
def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
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)
|
||||
@@ -736,6 +798,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():
|
||||
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)
|
||||
@@ -759,6 +822,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
|
||||
|
||||
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
flexmock(module).should_receive('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)
|
||||
@@ -782,6 +846,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
|
||||
|
||||
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
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)
|
||||
@@ -807,6 +872,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
|
||||
|
||||
def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
|
||||
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)
|
||||
@@ -833,6 +899,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||
|
||||
|
||||
def test_create_archive_with_source_directories_glob_expands():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
@@ -857,6 +924,7 @@ def test_create_archive_with_source_directories_glob_expands():
|
||||
|
||||
|
||||
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo*',))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
@@ -881,6 +949,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
|
||||
|
||||
|
||||
def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
@@ -904,6 +973,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||
|
||||
|
||||
def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||
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)
|
||||
@@ -926,6 +996,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||
|
||||
|
||||
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||
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)
|
||||
|
||||
@@ -7,8 +7,10 @@ from borgmatic.borg import extract as module
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command):
|
||||
flexmock(module).should_receive('execute_command').with_args(command).once()
|
||||
def insert_execute_command_mock(command, working_directory=None, error_on_warnings=True):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command, working_directory=working_directory, error_on_warnings=error_on_warnings
|
||||
).once()
|
||||
|
||||
|
||||
def insert_execute_command_output_mock(command, result):
|
||||
@@ -86,26 +88,28 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_restore_path_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=['path1', 'path2'],
|
||||
paths=['path1', 'path2'],
|
||||
location_config={},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_remote_path_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
remote_path='borg1',
|
||||
@@ -113,45 +117,49 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_numeric_owner_parameter():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--numeric-owner', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={'numeric_owner': True},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_umask_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={'umask': '0770'},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_lock_wait_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={'lock_wait': '5'},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -159,13 +167,14 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(
|
||||
('borg', 'extract', '--debug', '--list', '--show-rc', 'repo::archive')
|
||||
)
|
||||
@@ -175,35 +184,54 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_dry_run_parameter():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=True,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_destination_path():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest')
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
destination_path='/dest',
|
||||
)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_progress_parameter():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'extract', '--progress', 'repo::archive')
|
||||
('borg', 'extract', '--progress', 'repo::archive'),
|
||||
working_directory=None,
|
||||
error_on_warnings=True,
|
||||
).once()
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
restore_paths=None,
|
||||
paths=None,
|
||||
location_config={},
|
||||
storage_config={},
|
||||
progress=True,
|
||||
|
||||
@@ -14,7 +14,9 @@ def test_list_archives_calls_borg_with_parameters():
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -25,7 +27,9 @@ def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -36,7 +40,9 @@ def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +53,9 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=False)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -58,7 +66,9 @@ def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -71,7 +81,7 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config=storage_config,
|
||||
list_arguments=flexmock(archive=None, json=False),
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +94,7 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config=storage_config,
|
||||
list_arguments=flexmock(archive='archive', json=False),
|
||||
list_arguments=flexmock(archive='archive', json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +106,7 @@ def test_list_archives_with_local_path_calls_borg_via_local_path():
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False),
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
local_path='borg1',
|
||||
)
|
||||
|
||||
@@ -109,7 +119,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False),
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
@@ -122,7 +132,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter():
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, short=True),
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False, short=True),
|
||||
)
|
||||
|
||||
|
||||
@@ -137,7 +147,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter():
|
||||
'exclude',
|
||||
'exclude_from',
|
||||
'pattern',
|
||||
'pattern_from',
|
||||
'patterns_from',
|
||||
),
|
||||
)
|
||||
def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
||||
@@ -149,7 +159,22 @@ def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}),
|
||||
list_arguments=flexmock(
|
||||
archive=None, json=False, successful=False, **{argument_name: 'value'}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_successful_calls_borg_to_exclude_checkpoints():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--glob-archives', module.BORG_EXCLUDE_CHECKPOINTS_GLOB, 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=True),
|
||||
)
|
||||
|
||||
|
||||
@@ -159,7 +184,9 @@ def test_list_archives_with_json_calls_borg_with_json_parameter():
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.list_archives(
|
||||
repository='repo', storage_config={}, list_arguments=flexmock(archive=None, json=True)
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
|
||||
@@ -22,7 +22,10 @@ def test_run_configuration_runs_actions_for_each_repository():
|
||||
|
||||
def test_run_configuration_executes_hooks_for_create_action():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.hook).should_receive('execute_hook').twice()
|
||||
flexmock(module.command).should_receive('execute_hook').twice()
|
||||
flexmock(module.postgresql).should_receive('dump_databases').once()
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks').twice()
|
||||
flexmock(module.postgresql).should_receive('remove_database_dumps').once()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
@@ -32,7 +35,9 @@ def test_run_configuration_executes_hooks_for_create_action():
|
||||
|
||||
def test_run_configuration_logs_actions_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.hook).should_receive('execute_hook')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(module.postgresql).should_receive('dump_databases')
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks')
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
@@ -46,7 +51,7 @@ def test_run_configuration_logs_actions_error():
|
||||
|
||||
def test_run_configuration_logs_pre_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.hook).should_receive('execute_hook').and_raise(OSError).and_return(None)
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None)
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').never()
|
||||
@@ -60,7 +65,7 @@ def test_run_configuration_logs_pre_hook_error():
|
||||
|
||||
def test_run_configuration_logs_post_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.hook).should_receive('execute_hook').and_return(None).and_raise(
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
|
||||
OSError
|
||||
).and_return(None)
|
||||
expected_results = [flexmock()]
|
||||
@@ -76,7 +81,7 @@ def test_run_configuration_logs_post_hook_error():
|
||||
|
||||
def test_run_configuration_logs_on_error_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.hook).should_receive('execute_hook').and_raise(OSError)
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(
|
||||
expected_results[:1]
|
||||
@@ -148,7 +153,7 @@ def test_make_error_log_records_generates_nothing_for_other_error():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
flexmock(module.hook).should_receive('execute_hook').never()
|
||||
flexmock(module.command).should_receive('execute_hook').never()
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {}
|
||||
|
||||
@@ -208,7 +213,7 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
flexmock(module.hook).should_receive('execute_hook').and_raise(ValueError)
|
||||
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)}
|
||||
@@ -221,7 +226,7 @@ def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_post_hook_error():
|
||||
flexmock(module.hook).should_receive('execute_hook').and_return(None).and_raise(ValueError)
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError)
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import generate as module
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_with_examples():
|
||||
def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||
flexmock(module).should_receive('add_comments_to_configuration')
|
||||
flexmock(module).should_receive('add_comments_to_configuration_map')
|
||||
schema = {
|
||||
'map': OrderedDict(
|
||||
[
|
||||
@@ -35,3 +36,38 @@ def test_schema_to_sample_configuration_generates_config_with_examples():
|
||||
('section2', OrderedDict([('field2', 'Example 2'), ('field3', 'Example 3')])),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_strings_with_example():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
schema = {'seq': [{'type': 'str'}], 'example': ['hi']}
|
||||
|
||||
config = module._schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == ['hi']
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
schema = {
|
||||
'seq': [
|
||||
{
|
||||
'map': OrderedDict(
|
||||
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
config = module._schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
||||
schema = {'gobbledygook': [{'type': 'not-your'}]}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module._schema_to_sample_configuration(schema)
|
||||
|
||||
@@ -74,6 +74,27 @@ def test_apply_logical_validation_does_not_raise_otherwise():
|
||||
module.apply_logical_validation('config.yaml', {'retention': {'keep_secondly': 1000}})
|
||||
|
||||
|
||||
def test_remove_examples_strips_examples_from_map():
|
||||
schema = {
|
||||
'map': {
|
||||
'foo': {'desc': 'thing1', 'example': 'bar'},
|
||||
'baz': {'desc': 'thing2', 'example': 'quux'},
|
||||
}
|
||||
}
|
||||
|
||||
module.remove_examples(schema)
|
||||
|
||||
assert schema == {'map': {'foo': {'desc': 'thing1'}, 'baz': {'desc': 'thing2'}}}
|
||||
|
||||
|
||||
def test_remove_examples_strips_examples_from_sequence_of_maps():
|
||||
schema = {'seq': [{'map': {'foo': {'desc': 'thing', 'example': 'bar'}}, 'example': 'stuff'}]}
|
||||
|
||||
module.remove_examples(schema)
|
||||
|
||||
assert schema == {'seq': [{'map': {'foo': {'desc': 'thing'}}}]}
|
||||
|
||||
|
||||
def test_guard_configuration_contains_repository_does_not_raise_when_repository_in_config():
|
||||
module.guard_configuration_contains_repository(
|
||||
repository='repo', configurations={'config.yaml': {'location': {'repositories': ['repo']}}}
|
||||
|
||||
@@ -2,10 +2,27 @@ import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic import hook as module
|
||||
from borgmatic.hooks import command as module
|
||||
|
||||
|
||||
def test_interpolate_context_passes_through_command_without_variable():
|
||||
assert module.interpolate_context('ls', {'foo': 'bar'}) == 'ls'
|
||||
|
||||
|
||||
def test_interpolate_context_passes_through_command_with_unknown_variable():
|
||||
assert module.interpolate_context('ls {baz}', {'foo': 'bar'}) == 'ls {baz}'
|
||||
|
||||
|
||||
def test_interpolate_context_interpolates_variables():
|
||||
context = {'foo': 'bar', 'baz': 'quux'}
|
||||
|
||||
assert module.interpolate_context('ls {foo}{baz} {baz}', context) == 'ls barquux quux'
|
||||
|
||||
|
||||
def test_execute_hook_invokes_each_command():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.WARNING, shell=True
|
||||
).once()
|
||||
@@ -14,6 +31,9 @@ def test_execute_hook_invokes_each_command():
|
||||
|
||||
|
||||
def test_execute_hook_with_multiple_commands_invokes_each_command():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.WARNING, shell=True
|
||||
).once()
|
||||
@@ -25,6 +45,9 @@ def test_execute_hook_with_multiple_commands_invokes_each_command():
|
||||
|
||||
|
||||
def test_execute_hook_with_umask_sets_that_umask():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
|
||||
flexmock(module.os).should_receive('umask').with_args(0o22).once()
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
@@ -35,6 +58,9 @@ def test_execute_hook_with_umask_sets_that_umask():
|
||||
|
||||
|
||||
def test_execute_hook_with_dry_run_skips_commands():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').never()
|
||||
|
||||
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
|
||||
@@ -45,6 +71,9 @@ def test_execute_hook_with_empty_commands_does_not_raise():
|
||||
|
||||
|
||||
def test_execute_hook_on_error_logs_as_error():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.ERROR, shell=True
|
||||
).once()
|
||||
@@ -0,0 +1,17 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import cronitor as module
|
||||
|
||||
|
||||
def test_ping_cronitor_hits_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
append = 'failed-so-hard'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||
|
||||
module.ping_cronitor(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||
|
||||
|
||||
def test_ping_cronitor_without_ping_url_does_not_raise():
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_cronitor(ping_url=None, config_filename='config.yaml', dry_run=False, append='oops')
|
||||
@@ -0,0 +1,33 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import healthchecks as module
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args(ping_url)
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_without_ping_url_does_not_raise():
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_healthchecks(ping_url_or_uuid=None, config_filename='config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_with_ping_uuid_hits_corresponding_url():
|
||||
ping_uuid = 'abcd-efgh-ijkl-mnop'
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://hc-ping.com/{}'.format(ping_uuid)
|
||||
)
|
||||
|
||||
module.ping_healthchecks(ping_uuid, 'config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url_with_append():
|
||||
ping_url = 'https://example.com'
|
||||
append = 'failed-so-hard'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||
@@ -0,0 +1,388 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import postgresql as module
|
||||
|
||||
|
||||
def test_make_database_dump_filename_uses_name_and_hostname():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
assert module.make_database_dump_filename('test', 'hostname') == 'databases/hostname/test'
|
||||
|
||||
|
||||
def test_make_database_dump_filename_without_hostname_defaults_to_localhost():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
assert module.make_database_dump_filename('test') == 'databases/localhost/test'
|
||||
|
||||
|
||||
def test_make_database_dump_filename_with_invalid_name_raises():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.make_database_dump_filename('invalid/name')
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/{}'.format(name),
|
||||
'--format',
|
||||
'custom',
|
||||
name,
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_with_dry_run_skips_pg_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_dump_databases_without_databases_does_not_raise():
|
||||
module.dump_databases([], 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_hostname_and_port():
|
||||
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/database.example.org/foo'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/database.example.org/foo',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--format',
|
||||
'custom',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_username_and_password():
|
||||
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--username',
|
||||
'postgres',
|
||||
'--format',
|
||||
'custom',
|
||||
'foo',
|
||||
),
|
||||
extra_environment={'PGPASSWORD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_format():
|
||||
databases = [{'name': 'foo', 'format': 'tar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--format',
|
||||
'tar',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_options():
|
||||
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--format',
|
||||
'custom',
|
||||
'--stuff=such',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dumpall_for_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('pg_dumpall', '--no-password', '--clean', '--file', 'databases/localhost/all'),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_removes_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.os).should_receive('listdir').and_return([])
|
||||
flexmock(module.os).should_receive('rmdir')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'databases/localhost/{}'.format(name)
|
||||
).once()
|
||||
|
||||
module.remove_database_dumps(databases, '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('remove').never()
|
||||
|
||||
module.remove_database_dumps(databases, 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_remove_database_dumps_without_databases_does_not_raise():
|
||||
module.remove_database_dumps([], 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_make_database_dump_patterns_converts_names_to_glob_paths():
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/*/foo'
|
||||
).and_return('databases/*/bar')
|
||||
|
||||
assert module.make_database_dump_patterns(('foo', 'bar')) == [
|
||||
'databases/*/foo',
|
||||
'databases/*/bar',
|
||||
]
|
||||
|
||||
|
||||
def test_make_database_dump_patterns_treats_empty_names_as_matching_all_databases():
|
||||
flexmock(module).should_receive('make_database_dump_filename').with_args('*', '*').and_return(
|
||||
'databases/*/*'
|
||||
)
|
||||
|
||||
assert module.make_database_dump_patterns(()) == ['databases/*/*']
|
||||
|
||||
|
||||
def test_convert_glob_patterns_to_borg_patterns_removes_leading_slash():
|
||||
assert module.convert_glob_patterns_to_borg_patterns(('/etc/foo/bar',)) == ['sh:etc/foo/bar']
|
||||
|
||||
|
||||
def test_get_database_names_from_dumps_gets_names_from_filenames_matching_globs():
|
||||
flexmock(module.glob).should_receive('glob').and_return(
|
||||
('databases/localhost/foo',)
|
||||
).and_return(('databases/localhost/bar',)).and_return(())
|
||||
|
||||
assert module.get_database_names_from_dumps(
|
||||
('databases/*/foo', 'databases/*/bar', 'databases/*/baz')
|
||||
) == ['foo', 'bar']
|
||||
|
||||
|
||||
def test_get_database_configurations_only_produces_named_databases():
|
||||
databases = [
|
||||
{'name': 'foo', 'hostname': 'example.org'},
|
||||
{'name': 'bar', 'hostname': 'example.com'},
|
||||
{'name': 'baz', 'hostname': 'example.org'},
|
||||
]
|
||||
|
||||
assert list(module.get_database_configurations(databases, ('foo', 'baz'))) == [
|
||||
{'name': 'foo', 'hostname': 'example.org'},
|
||||
{'name': 'baz', 'hostname': 'example.org'},
|
||||
]
|
||||
|
||||
|
||||
def test_get_database_configurations_matches_all_database():
|
||||
databases = [
|
||||
{'name': 'foo', 'hostname': 'example.org'},
|
||||
{'name': 'all', 'hostname': 'example.com'},
|
||||
]
|
||||
|
||||
assert list(module.get_database_configurations(databases, ('foo', 'bar', 'baz'))) == [
|
||||
{'name': 'foo', 'hostname': 'example.org'},
|
||||
{'name': 'bar', 'hostname': 'example.com'},
|
||||
{'name': 'baz', 'hostname': 'example.com'},
|
||||
]
|
||||
|
||||
|
||||
def test_get_database_configurations_with_unknown_database_name_raises():
|
||||
databases = [{'name': 'foo', 'hostname': 'example.org'}]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
list(module.get_database_configurations(databases, ('foo', 'bar')))
|
||||
|
||||
|
||||
def test_restore_database_dumps_restores_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_restore',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--if-exists',
|
||||
'--exit-on-error',
|
||||
'--dbname',
|
||||
name,
|
||||
'databases/localhost/{}'.format(name),
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('psql', '--no-password', '--quiet', '--dbname', name, '--command', 'ANALYZE'),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dumps(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_restore_database_dumps_without_databases_does_not_raise():
|
||||
module.restore_database_dumps({}, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_restore_database_dumps_runs_pg_restore_with_hostname_and_port():
|
||||
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_restore',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--if-exists',
|
||||
'--exit-on-error',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--dbname',
|
||||
'foo',
|
||||
'databases/localhost/foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'psql',
|
||||
'--no-password',
|
||||
'--quiet',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--dbname',
|
||||
'foo',
|
||||
'--command',
|
||||
'ANALYZE',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dumps(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_restore_database_dumps_runs_pg_restore_with_username_and_password():
|
||||
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_restore',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--if-exists',
|
||||
'--exit-on-error',
|
||||
'--username',
|
||||
'postgres',
|
||||
'--dbname',
|
||||
'foo',
|
||||
'databases/localhost/foo',
|
||||
),
|
||||
extra_environment={'PGPASSWORD': 'trustsome1'},
|
||||
).once()
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'psql',
|
||||
'--no-password',
|
||||
'--quiet',
|
||||
'--username',
|
||||
'postgres',
|
||||
'--dbname',
|
||||
'foo',
|
||||
'--command',
|
||||
'ANALYZE',
|
||||
),
|
||||
extra_environment={'PGPASSWORD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dumps(databases, 'test.yaml', dry_run=False)
|
||||
+139
-4
@@ -6,10 +6,54 @@ from flexmock import flexmock
|
||||
from borgmatic import execute as module
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_error_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 2)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_warning_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 1)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_success_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 0)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_error_and_error_on_warnings_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 2, error_on_warnings=True)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_warning_and_error_on_warnings_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 1, error_on_warnings=True)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_success_and_error_on_warnings_is_false():
|
||||
assert not module.exit_code_indicates_error(
|
||||
('/usr/bin/borg1', 'init'), 0, error_on_warnings=True
|
||||
)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_error_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/command',), 2)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_warning_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/command',), 1)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_success_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/command',), 0)
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command, output_log_level=logging.INFO, shell=False
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command)
|
||||
@@ -19,8 +63,14 @@ def test_execute_command_calls_full_command():
|
||||
|
||||
def test_execute_command_calls_full_command_with_shell():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command, output_log_level=logging.INFO, shell=True
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=True,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, shell=True)
|
||||
@@ -28,11 +78,63 @@ def test_execute_command_calls_full_command_with_shell():
|
||||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command_with_extra_environment():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment={'a': 'b', 'c': 'd'},
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, extra_environment={'c': 'd'})
|
||||
|
||||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command_with_working_directory():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory='/working',
|
||||
error_on_warnings=False,
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, working_directory='/working')
|
||||
|
||||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command_with_error_on_warnings():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command,
|
||||
output_log_level=logging.INFO,
|
||||
shell=False,
|
||||
environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=True,
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, error_on_warnings=True)
|
||||
|
||||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_captures_output():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=False
|
||||
full_command, shell=False, env=None, cwd=None
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(full_command, output_log_level=None)
|
||||
@@ -43,8 +145,9 @@ def test_execute_command_captures_output():
|
||||
def test_execute_command_captures_output_with_shell():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=True
|
||||
full_command, shell=True, env=None, cwd=None
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(full_command, output_log_level=None, shell=True)
|
||||
@@ -52,6 +155,36 @@ def test_execute_command_captures_output_with_shell():
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_captures_output_with_extra_environment():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=False, env={'a': 'b', 'c': 'd'}, cwd=None
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(
|
||||
full_command, output_log_level=None, shell=False, extra_environment={'c': 'd'}
|
||||
)
|
||||
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_captures_output_with_working_directory():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=False, env=None, cwd='/working'
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(
|
||||
full_command, output_log_level=None, shell=False, working_directory='/working'
|
||||
)
|
||||
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_success():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(0, 'borg init')
|
||||
@@ -61,6 +194,7 @@ def test_execute_command_without_capture_does_not_raise_on_success():
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_warning():
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(False)
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'borg init')
|
||||
)
|
||||
@@ -69,6 +203,7 @@ def test_execute_command_without_capture_does_not_raise_on_warning():
|
||||
|
||||
|
||||
def test_execute_command_without_capture_raises_on_error():
|
||||
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(2, 'borg init')
|
||||
)
|
||||
|
||||
@@ -193,3 +193,33 @@ def test_configure_logging_skips_syslog_if_interactive_console():
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO)
|
||||
|
||||
|
||||
def test_configure_logging_to_logfile_instead_of_syslog():
|
||||
# syslog skipped in non-interactive console if --log-file argument provided
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.DEBUG, handlers=tuple
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
file_handler = logging.FileHandler('/tmp/logfile')
|
||||
flexmock(module.logging).should_receive('FileHandler').with_args('/tmp/logfile').and_return(
|
||||
file_handler
|
||||
).once()
|
||||
|
||||
module.configure_logging(
|
||||
console_log_level=logging.INFO, log_file_log_level=logging.DEBUG, log_file='/tmp/logfile'
|
||||
)
|
||||
|
||||
|
||||
def test_configure_logging_skips_logfile_if_argument_is_none():
|
||||
# No FileHandler added if argument --log-file is None
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.logging).should_receive('FileHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO, log_file=None)
|
||||
|
||||
@@ -17,6 +17,7 @@ def insert_logging_mock(log_level):
|
||||
def test_verbosity_to_log_level_maps_known_verbosity_to_log_level():
|
||||
assert module.verbosity_to_log_level(module.VERBOSITY_SOME) == logging.INFO
|
||||
assert module.verbosity_to_log_level(module.VERBOSITY_LOTS) == logging.DEBUG
|
||||
assert module.verbosity_to_log_level(module.VERBOSITY_ERROR) == logging.ERROR
|
||||
|
||||
|
||||
def test_verbosity_to_log_level_maps_unknown_verbosity_to_warning_level():
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[tox]
|
||||
envlist = py35,py36,py37
|
||||
envlist = py35,py36,py37,py38
|
||||
skip_missing_interpreters = True
|
||||
skipsdist = True
|
||||
minversion = 3.10.0
|
||||
minversion = 3.14.0
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
@@ -10,20 +10,17 @@ deps = -rtest_requirements.txt
|
||||
whitelist_externals =
|
||||
find
|
||||
sh
|
||||
install_command =
|
||||
sh scripts/pip {opts} {packages}
|
||||
commands_pre =
|
||||
find {toxinidir} -type f -not -path '{toxinidir}/.tox/*' -path '*/__pycache__/*' -name '*.py[c|o]' -delete
|
||||
commands =
|
||||
pytest {posargs}
|
||||
py36,py37: black --check .
|
||||
py36,py37,py38: black --check .
|
||||
isort --recursive --check-only --settings-path setup.cfg .
|
||||
flake8 borgmatic tests
|
||||
|
||||
[testenv:black]
|
||||
basepython = python3.7
|
||||
commands =
|
||||
black {posargs} .
|
||||
py36,py37,py38: black {posargs} .
|
||||
|
||||
[testenv:test]
|
||||
commands =
|
||||
|
||||
Reference in New Issue
Block a user