mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-08-06 16:23:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c12c47cace | ||
|
|
d6aaab8a09 | ||
|
|
128ebf04ce | ||
|
|
b1941bcce9 | ||
|
|
7b3b28616d | ||
|
|
f3910f49ca | ||
|
|
59e1cac92c | ||
|
|
b1f0287fdb | ||
|
|
99c35d4077 | ||
|
|
07b9ff61f2 | ||
|
|
f573c1810a | ||
|
|
1d37b14356 | ||
|
|
6c617eddd5 | ||
|
|
e14ebee4e0 | ||
|
|
a897ffd514 | ||
|
|
a472735616 | ||
|
|
b3fec03cf4 | ||
|
|
89dccc25c3 | ||
|
|
3846155d62 | ||
|
|
386979ebb4 | ||
|
|
07222cd984 | ||
|
|
cf4c6c274d |
+6
-6
@@ -1,30 +1,30 @@
|
|||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: python-3-5-alpine-3-9
|
name: python-3-5-alpine-3-10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: python:3.5-alpine3.9
|
image: python:3.5-alpine3.10
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
- scripts/run-tests
|
- scripts/run-tests
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: python-3-6-alpine-3-9
|
name: python-3-6-alpine-3-10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: python:3.6-alpine3.9
|
image: python:3.6-alpine3.10
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
- scripts/run-tests
|
- scripts/run-tests
|
||||||
---
|
---
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: python-3-7-alpine-3-9
|
name: python-3-7-alpine-3-10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build
|
- name: build
|
||||||
image: python:3.7-alpine3.9
|
image: python:3.7-alpine3.10
|
||||||
pull: always
|
pull: always
|
||||||
commands:
|
commands:
|
||||||
- scripts/run-tests
|
- scripts/run-tests
|
||||||
|
|||||||
+6
-1
@@ -16,7 +16,12 @@ module.exports = function(eleventyConfig) {
|
|||||||
// Replace links to .md files with links to directories. This allows unparsed Markdown links
|
// Replace links to .md files with links to directories. This allows unparsed Markdown links
|
||||||
// to work on GitHub, while rendered links elsewhere also work.
|
// to work on GitHub, while rendered links elsewhere also work.
|
||||||
replaceLink: function (link, env) {
|
replaceLink: function (link, env) {
|
||||||
return link.replace(/\.md$/, '/');
|
link = link.replace(/\.md$/, '/');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == "production") {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
return link.replace('https://torsion.org/borgmatic/', 'http://localhost:8080/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let markdownItAnchorOptions = {
|
let markdownItAnchorOptions = {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
include borgmatic/config/schema.yaml
|
include borgmatic/config/schema.yaml
|
||||||
|
graft sample/systemd
|
||||||
|
|||||||
@@ -1,3 +1,41 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
1.3.21
|
||||||
|
* #192: User-defined hooks for global setup or cleanup that run before/after all actions. See the
|
||||||
|
documentation for more information:
|
||||||
|
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
|
||||||
|
|
||||||
|
1.3.20
|
||||||
|
* #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO
|
||||||
|
priority, etc.
|
||||||
|
* #221: Fix "borgmatic create --progress" output so that it updates on the console in real-time.
|
||||||
|
|
||||||
|
1.3.19
|
||||||
|
* #219: Fix visibility of "borgmatic prune --stats" output.
|
||||||
|
|
||||||
1.3.18
|
1.3.18
|
||||||
* #220: Fix regression of argument parsing for default actions.
|
* #220: Fix regression of argument parsing for default actions.
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ 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/)
|
* [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/)
|
* [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/)
|
* [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/)
|
||||||
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/)
|
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/)
|
||||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
* [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/)
|
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||||
@@ -116,8 +117,3 @@ your thing. In general, contributions are very welcome. We don't bite!
|
|||||||
Also, please check out the [borgmatic development
|
Also, please check out the [borgmatic development
|
||||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||||
info on cloning source code, running tests, etc.
|
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>
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -163,6 +163,12 @@ def create_archive(
|
|||||||
+ sources
|
+ sources
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
return
|
||||||
|
|
||||||
if json:
|
if json:
|
||||||
output_log_level = None
|
output_log_level = None
|
||||||
elif stats:
|
elif stats:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from borgmatic.execute import execute_command
|
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -83,4 +83,10 @@ def extract_archive(
|
|||||||
+ (tuple(restore_paths) if restore_paths else ())
|
+ (tuple(restore_paths) if restore_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)
|
||||||
|
return
|
||||||
|
|
||||||
execute_command(full_command)
|
execute_command(full_command)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from borgmatic.execute import BORG_ERROR_EXIT_CODE, execute_command
|
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -45,8 +45,4 @@ def initialize_repository(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Don't use execute_command() here because it doesn't support interactive prompts.
|
# Don't use execute_command() here because it doesn't support interactive prompts.
|
||||||
try:
|
execute_command_without_capture(init_command)
|
||||||
subprocess.check_call(init_command)
|
|
||||||
except subprocess.CalledProcessError as error:
|
|
||||||
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
|
||||||
raise
|
|
||||||
|
|||||||
+10
-1
@@ -6,6 +6,11 @@ from borgmatic.execute import execute_command
|
|||||||
logger = logging.getLogger(__name__)
|
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):
|
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
|
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.
|
if an archive name is given, listing the files in that archive.
|
||||||
'''
|
'''
|
||||||
lock_wait = storage_config.get('lock_wait', None)
|
lock_wait = storage_config.get('lock_wait', None)
|
||||||
|
if list_arguments.successful:
|
||||||
|
list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB
|
||||||
|
|
||||||
full_command = (
|
full_command = (
|
||||||
(local_path, 'list')
|
(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('remote-path', remote_path)
|
||||||
+ make_flags('lock-wait', lock_wait)
|
+ 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))
|
'::'.join((repository, list_arguments.archive))
|
||||||
if list_arguments.archive
|
if list_arguments.archive
|
||||||
|
|||||||
@@ -64,4 +64,4 @@ def prune_archives(
|
|||||||
+ (repository,)
|
+ (repository,)
|
||||||
)
|
)
|
||||||
|
|
||||||
execute_command(full_command)
|
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
|
||||||
|
|||||||
@@ -316,6 +316,12 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
list_group.add_argument(
|
list_group.add_argument(
|
||||||
'-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
|
'-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(
|
list_group.add_argument(
|
||||||
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||||
)
|
)
|
||||||
@@ -323,7 +329,7 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
'--first', metavar='N', help='List first N archives after other filters are applied'
|
'--first', metavar='N', help='List first N archives after other filters are applied'
|
||||||
)
|
)
|
||||||
list_group.add_argument(
|
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(
|
list_group.add_argument(
|
||||||
'-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
|
'-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
|
||||||
@@ -388,6 +394,9 @@ def parse_arguments(*unparsed_arguments):
|
|||||||
if 'init' in arguments and arguments['global'].dry_run:
|
if 'init' in arguments and arguments['global'].dry_run:
|
||||||
raise ValueError('The init action cannot be used with the --dry-run option')
|
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 (
|
if (
|
||||||
'list' in arguments
|
'list' in arguments
|
||||||
and 'info' in arguments
|
and 'info' in arguments
|
||||||
|
|||||||
+147
-68
@@ -28,13 +28,16 @@ logger = logging.getLogger(__name__)
|
|||||||
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||||
|
|
||||||
|
|
||||||
def run_configuration(config_filename, config, arguments): # pragma: no cover
|
def run_configuration(config_filename, config, arguments):
|
||||||
'''
|
'''
|
||||||
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
||||||
dict from subparser name to a namespace of parsed arguments, execute its defined pruning,
|
dict from subparser name to a namespace of parsed arguments, execute its defined pruning,
|
||||||
backups, consistency checks, and/or other actions.
|
backups, consistency checks, and/or other actions.
|
||||||
|
|
||||||
Yield JSON output strings from executing any actions that produce JSON.
|
Yield a combination of:
|
||||||
|
|
||||||
|
* JSON output strings from successfully executing any actions that produce JSON
|
||||||
|
* logging.LogRecord instances containing errors from any actions or backup hooks that fail
|
||||||
'''
|
'''
|
||||||
(location, storage, retention, consistency, hooks) = (
|
(location, storage, retention, consistency, hooks) = (
|
||||||
config.get(section_name, {})
|
config.get(section_name, {})
|
||||||
@@ -42,12 +45,14 @@ def run_configuration(config_filename, config, arguments): # pragma: no cover
|
|||||||
)
|
)
|
||||||
global_arguments = arguments['global']
|
global_arguments = arguments['global']
|
||||||
|
|
||||||
try:
|
local_path = location.get('local_path', 'borg')
|
||||||
local_path = location.get('local_path', 'borg')
|
remote_path = location.get('remote_path')
|
||||||
remote_path = location.get('remote_path')
|
borg_environment.initialize(storage)
|
||||||
borg_environment.initialize(storage)
|
encountered_error = None
|
||||||
|
error_repository = ''
|
||||||
|
|
||||||
if 'create' in arguments:
|
if 'create' in arguments:
|
||||||
|
try:
|
||||||
hook.execute_hook(
|
hook.execute_hook(
|
||||||
hooks.get('before_backup'),
|
hooks.get('before_backup'),
|
||||||
hooks.get('umask'),
|
hooks.get('umask'),
|
||||||
@@ -55,20 +60,37 @@ def run_configuration(config_filename, config, arguments): # pragma: no cover
|
|||||||
'pre-backup',
|
'pre-backup',
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
|
hook.ping_healthchecks(
|
||||||
for repository_path in location['repositories']:
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
||||||
yield from run_actions(
|
)
|
||||||
arguments=arguments,
|
except (OSError, CalledProcessError) as error:
|
||||||
location=location,
|
encountered_error = error
|
||||||
storage=storage,
|
yield from make_error_log_records(
|
||||||
retention=retention,
|
'{}: Error running pre-backup hook'.format(config_filename), error
|
||||||
consistency=consistency,
|
|
||||||
local_path=local_path,
|
|
||||||
remote_path=remote_path,
|
|
||||||
repository_path=repository_path,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'create' in arguments:
|
if not encountered_error:
|
||||||
|
for repository_path in location['repositories']:
|
||||||
|
try:
|
||||||
|
yield from run_actions(
|
||||||
|
arguments=arguments,
|
||||||
|
location=location,
|
||||||
|
storage=storage,
|
||||||
|
retention=retention,
|
||||||
|
consistency=consistency,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
repository_path=repository_path,
|
||||||
|
)
|
||||||
|
except (OSError, CalledProcessError) 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(
|
hook.execute_hook(
|
||||||
hooks.get('after_backup'),
|
hooks.get('after_backup'),
|
||||||
hooks.get('umask'),
|
hooks.get('umask'),
|
||||||
@@ -76,15 +98,34 @@ def run_configuration(config_filename, config, arguments): # pragma: no cover
|
|||||||
'post-backup',
|
'post-backup',
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
except (OSError, CalledProcessError):
|
hook.ping_healthchecks(
|
||||||
hook.execute_hook(
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
||||||
hooks.get('on_error'),
|
)
|
||||||
hooks.get('umask'),
|
except (OSError, CalledProcessError) as error:
|
||||||
config_filename,
|
encountered_error = error
|
||||||
'on-error',
|
yield from make_error_log_records(
|
||||||
global_arguments.dry_run,
|
'{}: Error running post-backup hook'.format(config_filename), error
|
||||||
)
|
)
|
||||||
raise
|
|
||||||
|
if encountered_error:
|
||||||
|
try:
|
||||||
|
hook.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', ''),
|
||||||
|
)
|
||||||
|
hook.ping_healthchecks(
|
||||||
|
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
||||||
|
)
|
||||||
|
except (OSError, CalledProcessError) as error:
|
||||||
|
yield from make_error_log_records(
|
||||||
|
'{}: Error running on-error hook'.format(config_filename), error
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_actions(
|
def run_actions(
|
||||||
@@ -231,6 +272,38 @@ def load_configurations(config_filenames):
|
|||||||
return (configs, logs)
|
return (configs, logs)
|
||||||
|
|
||||||
|
|
||||||
|
def make_error_log_records(message, error=None):
|
||||||
|
'''
|
||||||
|
Given error message text and an optional exception object, yield a series of logging.LogRecord
|
||||||
|
instances with error summary information.
|
||||||
|
'''
|
||||||
|
if not error:
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
raise error
|
||||||
|
except CalledProcessError as error:
|
||||||
|
yield logging.makeLogRecord(
|
||||||
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||||
|
)
|
||||||
|
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(
|
||||||
|
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||||
|
)
|
||||||
|
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error))
|
||||||
|
except: # noqa: E722
|
||||||
|
# Raising above only as a means of determining the error type. Swallow the exception here
|
||||||
|
# because we don't want the exception to propagate out of this function.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def collect_configuration_run_summary_logs(configs, arguments):
|
def collect_configuration_run_summary_logs(configs, arguments):
|
||||||
'''
|
'''
|
||||||
Given a dict of configuration filename to corresponding parsed configuration, and parsed
|
Given a dict of configuration filename to corresponding parsed configuration, and parsed
|
||||||
@@ -253,16 +326,42 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
|||||||
try:
|
try:
|
||||||
validate.guard_configuration_contains_repository(repository, configs)
|
validate.guard_configuration_contains_repository(repository, configs)
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
yield logging.makeLogRecord(
|
yield from make_error_log_records(str(error))
|
||||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
return
|
||||||
)
|
|
||||||
|
if not configs:
|
||||||
|
yield from make_error_log_records(
|
||||||
|
'{}: No configuration files found'.format(' '.join(arguments['global'].config_paths))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'create' in arguments:
|
||||||
|
try:
|
||||||
|
for config_filename, config in configs.items():
|
||||||
|
hooks = config.get('hooks', {})
|
||||||
|
hook.execute_hook(
|
||||||
|
hooks.get('before_everything'),
|
||||||
|
hooks.get('umask'),
|
||||||
|
config_filename,
|
||||||
|
'pre-everything',
|
||||||
|
arguments['global'].dry_run,
|
||||||
|
)
|
||||||
|
except (CalledProcessError, ValueError, OSError) as error:
|
||||||
|
yield from make_error_log_records('Error running pre-everything hook', error)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Execute the actions corresponding to each configuration file.
|
# Execute the actions corresponding to each configuration file.
|
||||||
json_results = []
|
json_results = []
|
||||||
for config_filename, config in configs.items():
|
for config_filename, config in configs.items():
|
||||||
try:
|
results = list(run_configuration(config_filename, config, arguments))
|
||||||
json_results.extend(list(run_configuration(config_filename, config, arguments)))
|
error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
|
||||||
|
|
||||||
|
if error_logs:
|
||||||
|
yield from make_error_log_records(
|
||||||
|
'{}: Error running configuration file'.format(config_filename)
|
||||||
|
)
|
||||||
|
yield from error_logs
|
||||||
|
else:
|
||||||
yield logging.makeLogRecord(
|
yield logging.makeLogRecord(
|
||||||
dict(
|
dict(
|
||||||
levelno=logging.INFO,
|
levelno=logging.INFO,
|
||||||
@@ -270,45 +369,25 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
|||||||
msg='{}: Successfully ran configuration file'.format(config_filename),
|
msg='{}: Successfully ran configuration file'.format(config_filename),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except CalledProcessError as error:
|
if results:
|
||||||
yield logging.makeLogRecord(
|
json_results.extend(results)
|
||||||
dict(
|
|
||||||
levelno=logging.CRITICAL,
|
|
||||||
levelname='CRITICAL',
|
|
||||||
msg='{}: Error running configuration file'.format(config_filename),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
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(
|
|
||||||
dict(
|
|
||||||
levelno=logging.CRITICAL,
|
|
||||||
levelname='CRITICAL',
|
|
||||||
msg='{}: Error running configuration file'.format(config_filename),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
yield logging.makeLogRecord(
|
|
||||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
|
||||||
)
|
|
||||||
|
|
||||||
if json_results:
|
if json_results:
|
||||||
sys.stdout.write(json.dumps(json_results))
|
sys.stdout.write(json.dumps(json_results))
|
||||||
|
|
||||||
if not configs:
|
if 'create' in arguments:
|
||||||
yield logging.makeLogRecord(
|
try:
|
||||||
dict(
|
for config_filename, config in configs.items():
|
||||||
levelno=logging.CRITICAL,
|
hooks = config.get('hooks', {})
|
||||||
levelname='CRITICAL',
|
hook.execute_hook(
|
||||||
msg='{}: No configuration files found'.format(
|
hooks.get('after_everything'),
|
||||||
' '.join(arguments['global'].config_paths)
|
hooks.get('umask'),
|
||||||
),
|
config_filename,
|
||||||
)
|
'post-everything',
|
||||||
)
|
arguments['global'].dry_run,
|
||||||
|
)
|
||||||
|
except (CalledProcessError, ValueError, OSError) as error:
|
||||||
|
yield from make_error_log_records('Error running post-everything hook', error)
|
||||||
|
|
||||||
|
|
||||||
def exit_with_help_link(): # pragma: no cover
|
def exit_with_help_link(): # pragma: no cover
|
||||||
|
|||||||
@@ -337,31 +337,59 @@ map:
|
|||||||
example: false
|
example: false
|
||||||
hooks:
|
hooks:
|
||||||
desc: |
|
desc: |
|
||||||
Shell commands or scripts to execute before and after a backup or if an error has occurred.
|
Shell commands, scripts, or integrations to execute at various points during a borgmatic
|
||||||
IMPORTANT: All provided commands and scripts are executed with user permissions of borgmatic.
|
run. IMPORTANT: All provided commands and scripts are executed with user permissions of
|
||||||
Do not forget to set secure permissions on this file as well as on any script listed (chmod 0700) to
|
borgmatic. Do not forget to set secure permissions on this configuration file (chmod
|
||||||
prevent potential shell injection or privilege escalation.
|
0600) as well as on any script called from a hook (chmod 0700) to prevent potential
|
||||||
|
shell injection or privilege escalation.
|
||||||
map:
|
map:
|
||||||
before_backup:
|
before_backup:
|
||||||
seq:
|
seq:
|
||||||
- type: str
|
- type: str
|
||||||
desc: List of one or more shell commands or scripts to execute before creating a backup.
|
desc: |
|
||||||
|
List of one or more shell commands or scripts to execute before creating a
|
||||||
|
backup, run once per configuration file.
|
||||||
example:
|
example:
|
||||||
- echo "Starting a backup job."
|
- echo "Starting a backup."
|
||||||
after_backup:
|
after_backup:
|
||||||
seq:
|
seq:
|
||||||
- type: str
|
- type: str
|
||||||
desc: List of one or more shell commands or scripts to execute after creating a backup.
|
desc: |
|
||||||
|
List of one or more shell commands or scripts to execute after creating a
|
||||||
|
backup, run once per configuration file.
|
||||||
example:
|
example:
|
||||||
- echo "Backup created."
|
- echo "Created a backup."
|
||||||
on_error:
|
on_error:
|
||||||
seq:
|
seq:
|
||||||
- type: str
|
- type: str
|
||||||
desc: |
|
desc: |
|
||||||
List of one or more shell commands or scripts to execute when an exception occurs
|
List of one or more shell commands or scripts to execute when an exception
|
||||||
during a backup or when running a hook.
|
occurs during a backup or when running a before_backup or after_backup hook.
|
||||||
example:
|
example:
|
||||||
- echo "Error while creating a backup or running a hook."
|
- echo "Error while creating a backup or running a backup hook."
|
||||||
|
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
|
||||||
|
before_everything:
|
||||||
|
seq:
|
||||||
|
- type: str
|
||||||
|
desc: |
|
||||||
|
List of one or more shell commands or scripts to execute before running all
|
||||||
|
actions (if one of them is "create"), run once before all configuration files.
|
||||||
|
example:
|
||||||
|
- echo "Starting actions."
|
||||||
|
after_everything:
|
||||||
|
seq:
|
||||||
|
- type: str
|
||||||
|
desc: |
|
||||||
|
List of one or more shell commands or scripts to execute after running all
|
||||||
|
actions (if one of them is "create"), run once after all configuration files.
|
||||||
|
example:
|
||||||
|
- echo "Completed actions."
|
||||||
umask:
|
umask:
|
||||||
type: scalar
|
type: scalar
|
||||||
desc: Umask used when executing hooks. Defaults to the umask that borgmatic is run with.
|
desc: Umask used when executing hooks. Defaults to the umask that borgmatic is run with.
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
|||||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
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
|
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.
|
||||||
|
|
||||||
|
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||||
'''
|
'''
|
||||||
logger.debug(' '.join(full_command))
|
logger.debug(' '.join(full_command))
|
||||||
|
|
||||||
@@ -61,3 +63,18 @@ def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
|||||||
return output.decode() if output is not None else None
|
return output.decode() if output is not None else None
|
||||||
else:
|
else:
|
||||||
execute_and_log_output(full_command, output_log_level, shell=shell)
|
execute_and_log_output(full_command, output_log_level, shell=shell)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_command_without_capture(full_command):
|
||||||
|
'''
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
logger.debug(' '.join(full_command))
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call(full_command)
|
||||||
|
except subprocess.CalledProcessError as error:
|
||||||
|
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
||||||
|
raise
|
||||||
|
|||||||
+52
-1
@@ -1,18 +1,35 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from borgmatic import execute
|
from borgmatic import execute
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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,
|
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
|
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.
|
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 ValueError if the umask cannot be parsed.
|
||||||
|
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
||||||
'''
|
'''
|
||||||
if not commands:
|
if not commands:
|
||||||
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
|
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
|
||||||
@@ -20,6 +37,9 @@ def execute_hook(commands, umask, config_filename, description, dry_run):
|
|||||||
|
|
||||||
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
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:
|
if len(commands) == 1:
|
||||||
logger.info(
|
logger.info(
|
||||||
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
|
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
|
||||||
@@ -51,3 +71,34 @@ def execute_hook(commands, umask, config_filename, description, dry_run):
|
|||||||
finally:
|
finally:
|
||||||
if original_umask:
|
if original_umask:
|
||||||
os.umask(original_umask)
|
os.umask(original_umask)
|
||||||
|
|
||||||
|
|
||||||
|
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||||
|
'''
|
||||||
|
Ping the given healthchecks.io 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.io{}{}'.format(
|
||||||
|
config_filename, ' ' + append if append else '', dry_run_label
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logger.debug('{}: Using healthchecks.io ping URL {}'.format(config_filename, ping_url))
|
||||||
|
|
||||||
|
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||||
|
requests.get(ping_url)
|
||||||
|
|||||||
+3
-1
@@ -9,6 +9,8 @@ RUN borgmatic --help > /command-line.txt \
|
|||||||
|
|
||||||
FROM node:12.10.0-alpine as html
|
FROM node:12.10.0-alpine as html
|
||||||
|
|
||||||
|
ARG ENVIRONMENT=production
|
||||||
|
|
||||||
WORKDIR /source
|
WORKDIR /source
|
||||||
|
|
||||||
RUN npm install @11ty/eleventy \
|
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 /etc/borgmatic/config.yaml /source/docs/_includes/borgmatic/config.yaml
|
||||||
COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
|
COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
|
||||||
COPY . /source
|
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
|
&& mv /output/docs/index.html /output/index.html
|
||||||
|
|
||||||
FROM nginx:1.16.1-alpine
|
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>
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
{% include 'components/minilink.css' %}
|
{% include 'components/minilink.css' %}
|
||||||
{% include 'components/toc.css' %}
|
{% include 'components/toc.css' %}
|
||||||
{% include 'components/info-blocks.css' %}
|
{% include 'components/info-blocks.css' %}
|
||||||
|
{% include 'components/suggestion-form.css' %}
|
||||||
{% include 'prism-theme.css' %}
|
{% include 'prism-theme.css' %}
|
||||||
{% include 'asciinema.css' %}
|
{% include 'asciinema.css' %}
|
||||||
{% endset %}
|
{% endset %}
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ headerClass: elv-header-default
|
|||||||
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
|
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
|
||||||
<article>
|
<article>
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
|
|
||||||
|
{% include 'components/suggestion-form.html' %}
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -20,22 +20,38 @@ hooks:
|
|||||||
- rm /to/file.sql
|
- rm /to/file.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
borgmatic hooks run once per configuration file. `before_backup` hooks run
|
The `before_backup` and `after_backup` hooks each run once per configuration
|
||||||
prior to backups of all repositories. `after_backup` hooks run afterwards, but
|
file. `before_backup` hooks run prior to backups of all repositories in a
|
||||||
not if an error occurs in a previous hook or in the backups themselves.
|
configuration file, right before the `create` action. `after_backup` hooks run
|
||||||
|
afterwards, but not if an error occurs in a previous hook or in the backups
|
||||||
|
themselves.
|
||||||
|
|
||||||
|
You can also use `before_everything` and `after_everything` hooks to perform
|
||||||
## Error hooks
|
global setup or cleanup:
|
||||||
|
|
||||||
borgmatic also runs `on_error` hooks if an error occurs, either when creating
|
|
||||||
a backup or running another hook. Here's an example configuration:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
hooks:
|
hooks:
|
||||||
on_error:
|
before_everything:
|
||||||
- echo "Error while creating a backup or running a hook."
|
- set-up-stuff-globally
|
||||||
|
after_everything:
|
||||||
|
- clean-up-stuff-globally
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`before_everything` hooks collected from all borgmatic configuration files run
|
||||||
|
once before all configuration files (prior to all actions), but only if there
|
||||||
|
is a `create` action. An error encountered during a `before_everything` hook
|
||||||
|
causes borgmatic to exit without creating backups.
|
||||||
|
|
||||||
|
`after_everything` hooks run once after all configuration files and actions,
|
||||||
|
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.
|
||||||
|
|
||||||
|
borgmatic also runs `on_error` hooks if an error occurs, either when creating
|
||||||
|
a backup or running a backup hook. See the [monitoring and alerting
|
||||||
|
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups.md)
|
||||||
|
for more information.
|
||||||
|
|
||||||
## Hook output
|
## Hook output
|
||||||
|
|
||||||
Any output produced by your hooks shows up both at the console and in syslog
|
Any output produced by your hooks shows up both at the console and in syslog
|
||||||
@@ -48,7 +64,8 @@ your backups</a>.
|
|||||||
An important security note about hooks: borgmatic executes all hook commands
|
An important security note about hooks: borgmatic executes all hook commands
|
||||||
with the user permissions of borgmatic itself. So to prevent potential shell
|
with the user permissions of borgmatic itself. So to prevent potential shell
|
||||||
injection or privilege escalation, do not forget to set secure permissions
|
injection or privilege escalation, do not forget to set secure permissions
|
||||||
(`chmod 0700`) on borgmatic configuration files and scripts invoked by hooks.
|
on borgmatic configuration files (`chmod 0600`) and scripts (`chmod 0700`)
|
||||||
|
invoked by hooks.
|
||||||
|
|
||||||
|
|
||||||
## Related documentation
|
## Related documentation
|
||||||
@@ -56,3 +73,4 @@ injection or privilege escalation, do not forget to set secure permissions
|
|||||||
* [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.md)
|
||||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-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)
|
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||||
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups.md)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ repositories.
|
|||||||
|
|
||||||
If you find yourself in this situation, you have some options. First, you can
|
If you find yourself in this situation, you have some options. First, you can
|
||||||
run borgmatic's pruning, creating, or checking actions separately. For
|
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
|
```bash
|
||||||
borgmatic prune
|
borgmatic prune
|
||||||
@@ -25,7 +25,7 @@ borgmatic check
|
|||||||
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
|
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
|
||||||
`--prune`, `--create`, or `--check`. Or upgrade borgmatic!)
|
`--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
|
any number of them in a single borgmatic run. This supports approaches like
|
||||||
making backups with `create` on a frequent schedule, while only running
|
making backups with `create` on a frequent schedule, while only running
|
||||||
expensive consistency checks with `check` on a much less frequent basis from
|
expensive consistency checks with `check` on a much less frequent basis from
|
||||||
|
|||||||
@@ -20,9 +20,19 @@ Or, for even more progress and debug spew:
|
|||||||
borgmatic --verbosity 2
|
borgmatic --verbosity 2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backup summary
|
||||||
|
|
||||||
|
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 when performing a backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --stats
|
||||||
|
```
|
||||||
|
|
||||||
## Existing backups
|
## 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
|
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
|
||||||
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
||||||
functionality:
|
functionality:
|
||||||
@@ -36,6 +46,7 @@ borgmatic info
|
|||||||
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
|
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
|
||||||
`--info`. Or upgrade borgmatic!)
|
`--info`. Or upgrade borgmatic!)
|
||||||
|
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
||||||
@@ -72,18 +83,10 @@ Note that the [sample borgmatic systemd service
|
|||||||
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
|
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
|
||||||
already has this rate limit disabled.
|
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
|
## 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.md)
|
||||||
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups.md)
|
||||||
|
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md)
|
||||||
* [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.md)
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
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 Healthchecks hook**: This feature integrates with the
|
||||||
|
[Healthchecks](https://healthchecks.io/) service, and pings Healthchecks
|
||||||
|
whenever borgmatic runs. That way, Healthchecks can alert you when something
|
||||||
|
goes wrong or it doesn't hear from borgmatic for a configured interval. See
|
||||||
|
[Healthchecks
|
||||||
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-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 [restore
|
||||||
|
tests](https://torsion.org/borgmatic/docs/how-to/restore-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.md),
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
## 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.md)
|
||||||
|
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||||
|
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups.md)
|
||||||
|
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup.md)
|
||||||
|
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
|
||||||
@@ -65,3 +65,4 @@ Like a whole-archive restore, this also restores into the current directory.
|
|||||||
|
|
||||||
* [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.md)
|
||||||
* [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.md)
|
||||||
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups.md)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ To get up and running, first [install
|
|||||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at
|
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at
|
||||||
least version 1.1.
|
least version 1.1.
|
||||||
|
|
||||||
Borgmatic consumes configurations in `/etc/borgmatic/` and `/etc/borgmatic.d/`
|
By default, borgmatic looks for its configuration files in `/etc/borgmatic/`
|
||||||
by default. Therefore, we show how to install borgmatic for the root user which
|
and `/etc/borgmatic.d/`, where the root user typically has read access.
|
||||||
will have access permissions for these locations by default.
|
|
||||||
|
|
||||||
Run the following commands to download and install borgmatic:
|
So, to download and install borgmatic as the root user, run the following
|
||||||
|
commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 install --user --upgrade borgmatic
|
sudo pip3 install --user --upgrade borgmatic
|
||||||
@@ -35,10 +35,11 @@ borgmatic:
|
|||||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=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)
|
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||||
* [stand-alone binary](https://github.com/cmarquardt/borgmatic-binary)
|
* [stand-alone binary](https://github.com/cmarquardt/borgmatic-binary)
|
||||||
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
|
||||||
|
|
||||||
|
|
||||||
## Hosting providers
|
## Hosting providers
|
||||||
@@ -227,7 +228,7 @@ found character that cannot start any token
|
|||||||
in "config.yaml", line 230, column 1
|
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.
|
configuration file with the requisite number of spaces.
|
||||||
|
|
||||||
### libyaml compilation errors
|
### libyaml compilation errors
|
||||||
@@ -246,10 +247,6 @@ it.
|
|||||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups.md)
|
* [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)
|
* [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)
|
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||||
|
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups.md)
|
||||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration.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)
|
* [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>
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
|
||||||
|
|
||||||
0 3 * * * PATH=$PATH:/usr/bin /root/.local/bin/borgmatic
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
||||||
|
|
||||||
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
|
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=borgmatic backup
|
Description=borgmatic backup
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
ConditionACPower=true
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/root/.local/bin/borgmatic
|
|
||||||
|
# Lower CPU and I/O priority.
|
||||||
|
Nice=19
|
||||||
|
CPUSchedulingPolicy=batch
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
|
IOWeight=100
|
||||||
|
|
||||||
|
Restart=no
|
||||||
LogRateLimitIntervalSec=0
|
LogRateLimitIntervalSec=0
|
||||||
|
|
||||||
|
# Delay start to prevent backups running during boot.
|
||||||
|
ExecStartPre=sleep 1m
|
||||||
|
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||||
|
|||||||
+1
-2
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
docker build --tag borgmatic-docs --file docs/Dockerfile .
|
docker build --tag borgmatic-docs --build-arg ENVIRONMENT=dev --file docs/Dockerfile .
|
||||||
echo
|
echo
|
||||||
echo "You can view dev docs at http://localhost:8080"
|
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
|
echo
|
||||||
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.3.18'
|
VERSION = '1.3.26'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@@ -31,6 +31,7 @@ setup(
|
|||||||
obsoletes=['atticmatic'],
|
obsoletes=['atticmatic'],
|
||||||
install_requires=(
|
install_requires=(
|
||||||
'pykwalify>=1.6.0,<14.06',
|
'pykwalify>=1.6.0,<14.06',
|
||||||
|
'requests',
|
||||||
'ruamel.yaml>0.15.0,<0.17.0',
|
'ruamel.yaml>0.15.0,<0.17.0',
|
||||||
'setuptools',
|
'setuptools',
|
||||||
'colorama>=0.4.1,<0.5',
|
'colorama>=0.4.1,<0.5',
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ pytest==5.1.2
|
|||||||
pytest-cov==2.7.1
|
pytest-cov==2.7.1
|
||||||
python-dateutil==2.8.0
|
python-dateutil==2.8.0
|
||||||
PyYAML==5.1.2
|
PyYAML==5.1.2
|
||||||
|
requests==2.22.0
|
||||||
ruamel.yaml>0.15.0,<0.17.0
|
ruamel.yaml>0.15.0,<0.17.0
|
||||||
toml==0.10.0
|
toml==0.10.0
|
||||||
|
|||||||
@@ -230,6 +230,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():
|
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
|||||||
@@ -758,6 +758,29 @@ 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('_expand_directories').and_return(('foo', 'bar'))
|
||||||
|
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||||
|
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||||
|
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||||
|
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||||
|
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||||
|
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
|
||||||
|
)
|
||||||
|
|
||||||
|
module.create_archive(
|
||||||
|
dry_run=False,
|
||||||
|
repository='repo',
|
||||||
|
location_config={
|
||||||
|
'source_directories': ['foo', 'bar'],
|
||||||
|
'repositories': ['repo'],
|
||||||
|
'exclude_patterns': None,
|
||||||
|
},
|
||||||
|
storage_config={},
|
||||||
|
progress=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||||
|
|||||||
@@ -195,7 +195,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
|
|||||||
|
|
||||||
|
|
||||||
def test_extract_archive_calls_borg_with_progress_parameter():
|
def test_extract_archive_calls_borg_with_progress_parameter():
|
||||||
insert_execute_command_mock(('borg', 'extract', '--progress', 'repo::archive'))
|
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||||
|
('borg', 'extract', '--progress', 'repo::archive')
|
||||||
|
).once()
|
||||||
|
|
||||||
module.extract_archive(
|
module.extract_archive(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ def insert_info_command_not_found_mock():
|
|||||||
|
|
||||||
|
|
||||||
def insert_init_command_mock(init_command, **kwargs):
|
def insert_init_command_mock(init_command, **kwargs):
|
||||||
flexmock(module.subprocess).should_receive('check_call').with_args(
|
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||||
init_command, **kwargs
|
init_command
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
|
|
||||||
@@ -35,18 +35,9 @@ def test_initialize_repository_calls_borg_with_parameters():
|
|||||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_repository_does_not_raise_for_borg_init_warning():
|
|
||||||
insert_info_command_not_found_mock()
|
|
||||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
|
||||||
module.subprocess.CalledProcessError(1, 'borg init')
|
|
||||||
)
|
|
||||||
|
|
||||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_repository_raises_for_borg_init_error():
|
def test_initialize_repository_raises_for_borg_init_error():
|
||||||
insert_info_command_not_found_mock()
|
insert_info_command_not_found_mock()
|
||||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
flexmock(module).should_receive('execute_command_without_capture').and_raise(
|
||||||
module.subprocess.CalledProcessError(2, 'borg init')
|
module.subprocess.CalledProcessError(2, 'borg init')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,7 +47,7 @@ def test_initialize_repository_raises_for_borg_init_error():
|
|||||||
|
|
||||||
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
||||||
insert_info_command_found_mock()
|
insert_info_command_found_mock()
|
||||||
flexmock(module.subprocess).should_receive('check_call').never()
|
flexmock(module).should_receive('execute_command_without_capture').never()
|
||||||
|
|
||||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ def test_list_archives_calls_borg_with_parameters():
|
|||||||
)
|
)
|
||||||
|
|
||||||
module.list_archives(
|
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)
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
module.list_archives(
|
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)
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
module.list_archives(
|
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)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
module.list_archives(
|
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)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
module.list_archives(
|
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(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config=storage_config,
|
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(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config=storage_config,
|
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(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config={},
|
storage_config={},
|
||||||
list_arguments=flexmock(archive=None, json=False),
|
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||||
local_path='borg1',
|
local_path='borg1',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,7 +119,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
|
|||||||
module.list_archives(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config={},
|
storage_config={},
|
||||||
list_arguments=flexmock(archive=None, json=False),
|
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||||
remote_path='borg1',
|
remote_path='borg1',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,7 +132,7 @@ def test_list_archives_with_short_calls_borg_with_short_parameter():
|
|||||||
module.list_archives(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config={},
|
storage_config={},
|
||||||
list_arguments=flexmock(archive=None, json=False, short=True),
|
list_arguments=flexmock(archive=None, json=False, successful=False, short=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,7 +159,22 @@ def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
|||||||
module.list_archives(
|
module.list_archives(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
storage_config={},
|
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('[]')
|
).and_return('[]')
|
||||||
|
|
||||||
json_output = module.list_archives(
|
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 == '[]'
|
assert json_output == '[]'
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from borgmatic.borg import prune as module
|
|||||||
from ..test_verbosity import insert_logging_mock
|
from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
def insert_execute_command_mock(prune_command, **kwargs):
|
def insert_execute_command_mock(prune_command, output_log_level):
|
||||||
flexmock(module).should_receive('execute_command').with_args(prune_command).once()
|
flexmock(module).should_receive('execute_command').with_args(
|
||||||
|
prune_command, output_log_level=output_log_level
|
||||||
|
).once()
|
||||||
|
|
||||||
|
|
||||||
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
|
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
|
||||||
@@ -61,7 +63,7 @@ def test_prune_archives_calls_borg_with_parameters():
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',))
|
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
|
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
|
||||||
@@ -73,7 +75,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'))
|
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
|
||||||
insert_logging_mock(logging.INFO)
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
@@ -87,7 +89,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
|
|||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(
|
insert_execute_command_mock(
|
||||||
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo')
|
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo'), logging.INFO
|
||||||
)
|
)
|
||||||
insert_logging_mock(logging.DEBUG)
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'))
|
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
|
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
|
||||||
@@ -113,7 +115,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',))
|
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
@@ -129,7 +131,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'))
|
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
@@ -140,13 +142,29 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
|
||||||
|
retention_config = flexmock()
|
||||||
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
|
BASE_PRUNE_FLAGS
|
||||||
|
)
|
||||||
|
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING)
|
||||||
|
|
||||||
|
module.prune_archives(
|
||||||
|
dry_run=False,
|
||||||
|
repository='repo',
|
||||||
|
storage_config={},
|
||||||
|
retention_config=retention_config,
|
||||||
|
stats=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
|
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
|
||||||
storage_config = {'umask': '077'}
|
storage_config = {'umask': '077'}
|
||||||
retention_config = flexmock()
|
retention_config = flexmock()
|
||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'))
|
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
@@ -162,7 +180,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
|||||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||||
BASE_PRUNE_FLAGS
|
BASE_PRUNE_FLAGS
|
||||||
)
|
)
|
||||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'))
|
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
|
||||||
|
|
||||||
module.prune_archives(
|
module.prune_archives(
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
@@ -5,6 +6,90 @@ from flexmock import flexmock
|
|||||||
from borgmatic.commands import borgmatic as module
|
from borgmatic.commands import borgmatic as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_configuration_runs_actions_for_each_repository():
|
||||||
|
flexmock(module.borg_environment).should_receive('initialize')
|
||||||
|
expected_results = [flexmock(), flexmock()]
|
||||||
|
flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
|
||||||
|
expected_results[1:]
|
||||||
|
)
|
||||||
|
config = {'location': {'repositories': ['foo', 'bar']}}
|
||||||
|
arguments = {'global': flexmock()}
|
||||||
|
|
||||||
|
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
assert results == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
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).should_receive('run_actions').and_return([])
|
||||||
|
config = {'location': {'repositories': ['foo']}}
|
||||||
|
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||||
|
|
||||||
|
list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_configuration_logs_actions_error():
|
||||||
|
flexmock(module.borg_environment).should_receive('initialize')
|
||||||
|
flexmock(module.hook).should_receive('execute_hook')
|
||||||
|
expected_results = [flexmock()]
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||||
|
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||||
|
config = {'location': {'repositories': ['foo']}}
|
||||||
|
arguments = {'global': flexmock(dry_run=False)}
|
||||||
|
|
||||||
|
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
assert results == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
expected_results = [flexmock()]
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||||
|
flexmock(module).should_receive('run_actions').never()
|
||||||
|
config = {'location': {'repositories': ['foo']}}
|
||||||
|
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||||
|
|
||||||
|
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
assert results == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
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').and_return([])
|
||||||
|
config = {'location': {'repositories': ['foo']}}
|
||||||
|
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||||
|
|
||||||
|
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
assert results == expected_results
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
expected_results = [flexmock(), flexmock()]
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(
|
||||||
|
expected_results[:1]
|
||||||
|
).and_return(expected_results[1:])
|
||||||
|
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||||
|
config = {'location': {'repositories': ['foo']}}
|
||||||
|
arguments = {'global': flexmock(dry_run=False)}
|
||||||
|
|
||||||
|
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||||
|
|
||||||
|
assert results == expected_results
|
||||||
|
|
||||||
|
|
||||||
def test_load_configurations_collects_parsed_configurations():
|
def test_load_configurations_collects_parsed_configurations():
|
||||||
configuration = flexmock()
|
configuration = flexmock()
|
||||||
other_configuration = flexmock()
|
other_configuration = flexmock()
|
||||||
@@ -24,10 +109,46 @@ def test_load_configurations_logs_critical_for_parse_error():
|
|||||||
configs, logs = tuple(module.load_configurations(('test.yaml',)))
|
configs, logs = tuple(module.load_configurations(('test.yaml',)))
|
||||||
|
|
||||||
assert configs == {}
|
assert configs == {}
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_error_log_records_generates_output_logs_for_message_only():
|
||||||
|
logs = tuple(module.make_error_log_records('Error'))
|
||||||
|
|
||||||
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_error_log_records_generates_output_logs_for_called_process_error():
|
||||||
|
logs = tuple(
|
||||||
|
module.make_error_log_records(
|
||||||
|
'Error', subprocess.CalledProcessError(1, 'ls', 'error output')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
|
assert any(log for log in logs if 'error output' in str(log))
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_error_log_records_generates_logs_for_value_error():
|
||||||
|
logs = tuple(module.make_error_log_records('Error', ValueError()))
|
||||||
|
|
||||||
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_error_log_records_generates_logs_for_os_error():
|
||||||
|
logs = tuple(module.make_error_log_records('Error', OSError()))
|
||||||
|
|
||||||
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_error_log_records_generates_nothing_for_other_error():
|
||||||
|
logs = tuple(module.make_error_log_records('Error', KeyError()))
|
||||||
|
|
||||||
|
assert logs == ()
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||||
|
flexmock(module.hook).should_receive('execute_hook').never()
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
arguments = {}
|
arguments = {}
|
||||||
|
|
||||||
@@ -35,7 +156,18 @@ def test_collect_configuration_run_summary_logs_info_for_success():
|
|||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert {log.levelno for log in logs} == {logging.INFO}
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||||
|
flexmock(module).should_receive('run_configuration').and_return([])
|
||||||
|
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||||
|
|
||||||
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {log.levelno for log in logs} == {logging.INFO}
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
||||||
@@ -47,33 +179,74 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
|||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert {log.levelno for log in logs} == {logging.INFO}
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_extract_with_repository_error():
|
def test_collect_configuration_run_summary_logs_extract_with_repository_error():
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||||
ValueError
|
ValueError
|
||||||
)
|
)
|
||||||
|
expected_logs = (flexmock(),)
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||||
arguments = {'extract': flexmock(repository='repo')}
|
arguments = {'extract': flexmock(repository='repo')}
|
||||||
|
|
||||||
logs = tuple(
|
logs = tuple(
|
||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert logs == expected_logs
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_and_repository_error():
|
def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||||
|
arguments = {'global': flexmock(config_paths=[])}
|
||||||
|
expected_logs = (flexmock(),)
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||||
|
|
||||||
|
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
|
||||||
|
|
||||||
|
assert logs == expected_logs
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||||
|
flexmock(module.hook).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)}
|
||||||
|
|
||||||
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert logs == expected_logs
|
||||||
|
|
||||||
|
|
||||||
|
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).should_receive('run_configuration').and_return([])
|
||||||
|
expected_logs = (flexmock(),)
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||||
|
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||||
|
|
||||||
|
logs = tuple(
|
||||||
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_logs[0] in logs
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_configuration_run_summary_logs_for_list_with_archive_and_repository_error():
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||||
ValueError
|
ValueError
|
||||||
)
|
)
|
||||||
|
expected_logs = (flexmock(),)
|
||||||
|
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||||
arguments = {'list': flexmock(repository='repo', archive='test')}
|
arguments = {'list': flexmock(repository='repo', archive='test')}
|
||||||
|
|
||||||
logs = tuple(
|
logs = tuple(
|
||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert logs == expected_logs
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
||||||
@@ -84,25 +257,13 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
|||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
assert {log.levelno for log in logs} == {logging.INFO}
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_run_value_error():
|
def test_collect_configuration_run_summary_logs_run_configuration_error():
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||||
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
|
flexmock(module).should_receive('run_configuration').and_return(
|
||||||
arguments = {}
|
[logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
|
||||||
|
|
||||||
logs = tuple(
|
|
||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_including_output_for_run_process_error():
|
|
||||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
|
||||||
flexmock(module).should_receive('run_configuration').and_raise(
|
|
||||||
subprocess.CalledProcessError(1, 'command', 'error output')
|
|
||||||
)
|
)
|
||||||
arguments = {}
|
arguments = {}
|
||||||
|
|
||||||
@@ -110,8 +271,7 @@ def test_collect_configuration_run_summary_logs_critical_including_output_for_ru
|
|||||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||||
)
|
)
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||||
assert any(log for log in logs if 'error output' in str(log))
|
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||||
@@ -126,12 +286,3 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
|||||||
{'test.yaml': {}, 'test2.yaml': {}}, arguments=arguments
|
{'test.yaml': {}, 'test2.yaml': {}}, arguments=arguments
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
|
|
||||||
flexmock(module).should_receive('run_configuration').and_return([])
|
|
||||||
arguments = {'global': flexmock(config_paths=[])}
|
|
||||||
|
|
||||||
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
|
|
||||||
|
|
||||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic import execute as module
|
from borgmatic import execute as module
|
||||||
@@ -49,3 +50,28 @@ def test_execute_command_captures_output_with_shell():
|
|||||||
output = module.execute_command(full_command, output_log_level=None, shell=True)
|
output = module.execute_command(full_command, output_log_level=None, shell=True)
|
||||||
|
|
||||||
assert output == expected_output
|
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')
|
||||||
|
)
|
||||||
|
|
||||||
|
module.execute_command_without_capture(('borg', 'init'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_command_without_capture_does_not_raise_on_warning():
|
||||||
|
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||||
|
module.subprocess.CalledProcessError(1, 'borg init')
|
||||||
|
)
|
||||||
|
|
||||||
|
module.execute_command_without_capture(('borg', 'init'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_command_without_capture_raises_on_error():
|
||||||
|
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||||
|
module.subprocess.CalledProcessError(2, 'borg init')
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(module.subprocess.CalledProcessError):
|
||||||
|
module.execute_command_without_capture(('borg', 'init'))
|
||||||
|
|||||||
@@ -5,7 +5,24 @@ from flexmock import flexmock
|
|||||||
from borgmatic import hook as module
|
from borgmatic import hook 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():
|
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(
|
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||||
[':'], output_log_level=logging.WARNING, shell=True
|
[':'], output_log_level=logging.WARNING, shell=True
|
||||||
).once()
|
).once()
|
||||||
@@ -14,6 +31,9 @@ def test_execute_hook_invokes_each_command():
|
|||||||
|
|
||||||
|
|
||||||
def test_execute_hook_with_multiple_commands_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(
|
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||||
[':'], output_log_level=logging.WARNING, shell=True
|
[':'], output_log_level=logging.WARNING, shell=True
|
||||||
).once()
|
).once()
|
||||||
@@ -25,6 +45,9 @@ def test_execute_hook_with_multiple_commands_invokes_each_command():
|
|||||||
|
|
||||||
|
|
||||||
def test_execute_hook_with_umask_sets_that_umask():
|
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(0o77).and_return(0o22).once()
|
||||||
flexmock(module.os).should_receive('umask').with_args(0o22).once()
|
flexmock(module.os).should_receive('umask').with_args(0o22).once()
|
||||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
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():
|
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()
|
flexmock(module.execute).should_receive('execute_command').never()
|
||||||
|
|
||||||
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
|
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
|
||||||
@@ -45,8 +71,41 @@ def test_execute_hook_with_empty_commands_does_not_raise():
|
|||||||
|
|
||||||
|
|
||||||
def test_execute_hook_on_error_logs_as_error():
|
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(
|
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||||
[':'], output_log_level=logging.ERROR, shell=True
|
[':'], output_log_level=logging.ERROR, shell=True
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
|
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user