Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78ff734e6c | ||
|
|
2cc743cf47 | ||
|
|
d99e6d1994 | ||
|
|
50f62d73b7 | ||
|
|
26a89de790 | ||
|
|
c2276b18c5 | ||
|
|
693434f8aa | ||
|
|
1e8edc05e9 | ||
|
|
1f166a47e9 | ||
|
|
9ee6151999 |
@@ -32,6 +32,8 @@ module.exports = function(eleventyConfig) {
|
||||
.use(markdownItReplaceLink)
|
||||
);
|
||||
|
||||
eleventyConfig.addPassthroughCopy({"docs/static": "static"});
|
||||
|
||||
return {
|
||||
templateFormats: [
|
||||
"md",
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
1.4.12
|
||||
* #247: With "borgmatic check", consider Borg warnings as errors.
|
||||
* Dial back the display of inline error logs a bit, so failed command output doesn't appear
|
||||
multiple times in the logs (well, except for the summary).
|
||||
|
||||
1.4.11
|
||||
* #241: When using the Healthchecks monitoring hook, include borgmatic logs in the payloads for
|
||||
completion and failure pings.
|
||||
* With --verbosity level 1 or 2, show error logs both inline when they occur and in the summary
|
||||
logs at the bottom. With lower verbosity levels, suppress the summary and show error logs when
|
||||
they occur.
|
||||
|
||||
1.4.10
|
||||
* #246: Fix for "borgmatic restore" showing success and incorrectly extracting archive files, even
|
||||
when no databases are configured to restore. As this can overwrite files from the archive and
|
||||
|
||||
@@ -2,68 +2,71 @@
|
||||
title: borgmatic
|
||||
permalink: index.html
|
||||
---
|
||||
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
||||
|
||||
## Overview
|
||||
## It's your data. Keep it that way.
|
||||
|
||||
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
|
||||
<img src="docs/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
|
||||
|
||||
borgmatic is simple, configuration-driven backup software for servers and
|
||||
workstations. Backup all of your machines from the command-line or scheduled
|
||||
jobs. No GUI required. Built atop [Borg Backup](https://www.borgbackup.org/),
|
||||
borgmatic initiates a backup, prunes any old backups according to a retention
|
||||
policy, and validates backups for consistency. borgmatic supports specifying
|
||||
your settings in a declarative configuration file, rather than having to put
|
||||
them all on the command-line, and handles common errors.
|
||||
workstations. Protect your files with client-side encryption. Backup your
|
||||
databases too. Monitor it all with integrated third-party services.
|
||||
|
||||
Here's an example config file:
|
||||
Here's an example configuration file:
|
||||
|
||||
```yaml
|
||||
location:
|
||||
# List of source directories to backup. Globs are expanded.
|
||||
# List of source directories to backup.
|
||||
source_directories:
|
||||
- /home
|
||||
- /etc
|
||||
- /var/log/syslog*
|
||||
|
||||
# Paths to local or remote repositories.
|
||||
repositories:
|
||||
- user@backupserver:sourcehostname.borg
|
||||
|
||||
# Any paths matching these patterns are excluded from backups.
|
||||
exclude_patterns:
|
||||
- /home/*/.cache
|
||||
|
||||
retention:
|
||||
# Retention policy for how many backups to keep in each category.
|
||||
# Retention policy for how many backups to keep.
|
||||
keep_daily: 7
|
||||
keep_weekly: 4
|
||||
keep_monthly: 6
|
||||
|
||||
consistency:
|
||||
# List of consistency checks to run: "repository", "archives", etc.
|
||||
# List of checks to run to validate your backups.
|
||||
checks:
|
||||
- repository
|
||||
- archives
|
||||
|
||||
hooks:
|
||||
# Preparation scripts to run, databases to dump, and monitoring to perform.
|
||||
# Custom preparation scripts to run.
|
||||
before_backup:
|
||||
- prepare-for-backup.sh
|
||||
|
||||
# Databases to dump and include in backups.
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
|
||||
# Third-party services to notify you if backups aren't happening.
|
||||
healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
|
||||
```
|
||||
|
||||
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
|
||||
available](https://projects.torsion.org/witten/borgmatic). It's also mirrored
|
||||
on [GitHub](https://github.com/witten/borgmatic) for convenience.
|
||||
|
||||
Want to see borgmatic in action? Check out the <a
|
||||
href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
|
||||
|
||||
<script src="https://asciinema.org/a/203761.js" id="asciicast-203761" async></script>
|
||||
|
||||
borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
|
||||
## Integrations
|
||||
|
||||
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.rsync.net/cgi-bin/borg.cgi?campaign=borg&adgroup=borgmatic"><img src="docs/static/rsyncnet.png" alt="rsync.net" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px;"></a>
|
||||
|
||||
|
||||
## How-to guides
|
||||
|
||||
@@ -117,6 +120,10 @@ Other questions or comments? Contact <mailto:witten@torsion.org>.
|
||||
|
||||
### Contributing
|
||||
|
||||
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
|
||||
available](https://projects.torsion.org/witten/borgmatic). It's also mirrored
|
||||
on [GitHub](https://github.com/witten/borgmatic) for convenience.
|
||||
|
||||
If you'd like to contribute to borgmatic development, please feel free to
|
||||
submit a [Pull Request](https://projects.torsion.org/witten/borgmatic/pulls)
|
||||
or open an [issue](https://projects.torsion.org/witten/borgmatic/issues) first
|
||||
@@ -126,3 +133,6 @@ your thing. In general, contributions are very welcome. We don't bite!
|
||||
Also, please check out the [borgmatic development
|
||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||
info on cloning source code, running tests, etc.
|
||||
|
||||
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ def check_archives(
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command)
|
||||
execute_command(full_command, error_on_warnings=True)
|
||||
|
||||
if 'extract' in checks:
|
||||
extract.extract_last_archive_dry_run(repository, lock_wait, local_path, remote_path)
|
||||
|
||||
@@ -365,33 +365,38 @@ def load_configurations(config_filenames):
|
||||
return (configs, logs)
|
||||
|
||||
|
||||
def log_record(**kwargs):
|
||||
'''
|
||||
Create a log record based on the given makeLogRecord() arguments, one of which must be
|
||||
named "levelno". Log the record and return it.
|
||||
'''
|
||||
record = logging.makeLogRecord(kwargs)
|
||||
logger.handle(record)
|
||||
|
||||
return record
|
||||
|
||||
|
||||
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.
|
||||
instances with error summary information. As a side effect, log each record.
|
||||
'''
|
||||
if not error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
)
|
||||
yield log_record(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 log_record(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
if error.output:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output)
|
||||
)
|
||||
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error))
|
||||
yield log_record(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))
|
||||
yield log_record(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
yield log_record(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.
|
||||
@@ -535,13 +540,14 @@ def main(): # pragma: no cover
|
||||
|
||||
summary_logs = list(collect_configuration_run_summary_logs(configs, arguments))
|
||||
|
||||
logger.info('')
|
||||
logger.info('summary:')
|
||||
[
|
||||
logger.handle(log)
|
||||
for log in parse_logs + summary_logs
|
||||
if log.levelno >= logger.getEffectiveLevel()
|
||||
]
|
||||
if logger.getEffectiveLevel() < logging.WARNING:
|
||||
logger.info('')
|
||||
logger.info('summary:')
|
||||
[
|
||||
logger.handle(log)
|
||||
for log in parse_logs + summary_logs
|
||||
if log.levelno >= logger.getEffectiveLevel()
|
||||
]
|
||||
|
||||
if any(log.levelno == logging.CRITICAL for log in summary_logs):
|
||||
exit_with_help_link()
|
||||
|
||||
@@ -12,6 +12,57 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
|
||||
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
||||
PAYLOAD_LIMIT_BYTES = 10 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR)
|
||||
|
||||
|
||||
class Forgetful_buffering_handler(logging.Handler):
|
||||
'''
|
||||
A buffering log handler that stores log messages in memory, and throws away messages (oldest
|
||||
first) once a particular capacity in bytes is reached.
|
||||
'''
|
||||
|
||||
def __init__(self, byte_capacity):
|
||||
super().__init__()
|
||||
|
||||
self.byte_capacity = byte_capacity
|
||||
self.byte_count = 0
|
||||
self.buffer = []
|
||||
self.forgot = False
|
||||
|
||||
def emit(self, record):
|
||||
message = record.getMessage() + '\n'
|
||||
self.byte_count += len(message)
|
||||
self.buffer.append(message)
|
||||
|
||||
while self.byte_count > self.byte_capacity and self.buffer:
|
||||
self.byte_count -= len(self.buffer[0])
|
||||
self.buffer.pop(0)
|
||||
self.forgot = True
|
||||
|
||||
|
||||
def format_buffered_logs_for_payload():
|
||||
'''
|
||||
Get the handler previously added to the root logger, and slurp buffered logs out of it to
|
||||
send to Healthchecks.
|
||||
'''
|
||||
try:
|
||||
buffering_handler = next(
|
||||
handler
|
||||
for handler in logging.getLogger().handlers
|
||||
if isinstance(handler, Forgetful_buffering_handler)
|
||||
)
|
||||
except StopIteration:
|
||||
# No handler means no payload.
|
||||
return ''
|
||||
|
||||
payload = ''.join(message for message in buffering_handler.buffer)
|
||||
|
||||
if buffering_handler.forgot:
|
||||
return PAYLOAD_TRUNCATION_INDICATOR + payload
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def ping_monitor(ping_url_or_uuid, config_filename, state, dry_run):
|
||||
'''
|
||||
@@ -19,6 +70,12 @@ def ping_monitor(ping_url_or_uuid, config_filename, state, dry_run):
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||
anything.
|
||||
'''
|
||||
if state is monitor.State.START:
|
||||
# Add a handler to the root logger that stores in memory the most recent logs emitted. That
|
||||
# way, we can send them all to Healthchecks upon a finish or failure state.
|
||||
logging.getLogger().addHandler(Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES))
|
||||
payload = ''
|
||||
|
||||
ping_url = (
|
||||
ping_url_or_uuid
|
||||
if ping_url_or_uuid.startswith('http')
|
||||
@@ -35,6 +92,9 @@ def ping_monitor(ping_url_or_uuid, config_filename, state, dry_run):
|
||||
)
|
||||
logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
if state in (monitor.State.FINISH, monitor.State.FAIL):
|
||||
payload = format_buffered_logs_for_payload()
|
||||
|
||||
if not dry_run:
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
requests.get(ping_url)
|
||||
requests.post(ping_url, data=payload)
|
||||
|
||||
@@ -173,6 +173,24 @@ after backups. So if necessary, you can use these hooks to create database
|
||||
dumps with any database system.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### MySQL table lock errors
|
||||
|
||||
If you encounter table lock errors during a database dump with MySQL/MariaDB,
|
||||
you may need to [use a
|
||||
transaction](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html#option_mysqldump_single-transaction).
|
||||
You can add any additional flags to the `options:` in your database
|
||||
configuration. Here's an example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
mysql_databases:
|
||||
- name: posts
|
||||
options: "--single-transaction --quick"
|
||||
```
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
||||
@@ -57,10 +57,10 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
|
||||
|
||||
## Error hooks
|
||||
|
||||
When an error occurs during a backup, borgmatic can run configurable shell
|
||||
commands to fire off custom error notifications or take other actions, so you
|
||||
can get alerted as soon as something goes wrong. Here's a not-so-useful
|
||||
example:
|
||||
When an error occurs during a backup or another action, 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:
|
||||
@@ -91,9 +91,10 @@ here:
|
||||
* `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
|
||||
Note that borgmatic runs the `on_error` hooks for any action in which an error
|
||||
occurs, not just the `create` action. But borgmatic does not run `on_error`
|
||||
hooks if an error occurs within a `before_everything` or `after_everything`
|
||||
hook. For more about hooks, see the [borgmatic hooks
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
especially the security information.
|
||||
|
||||
@@ -116,9 +117,21 @@ With this hook in place, borgmatic pings your Healthchecks project when a
|
||||
backup begins, ends, or errors. Specifically, before the <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||
hooks</a> run, borgmatic lets Healthchecks know that a backup has started.
|
||||
|
||||
Then, if the backup completes successfully, borgmatic notifies Healthchecks of
|
||||
the success after the `after_backup` hooks run. And if an error occurs during
|
||||
the backup, borgmatic notifies Healthchecks after the `on_error` hooks run.
|
||||
the success after the `after_backup` hooks run, and includes borgmatic logs in
|
||||
the payload data sent to Healthchecks. This means that borgmatic logs show up
|
||||
in the Healthchecks UI, although be aware that Healthchecks currently has a
|
||||
10-kilobyte limit for the logs in each ping.
|
||||
|
||||
If an error occurs during the backup, borgmatic notifies Healthchecks after
|
||||
the `on_error` hooks run, also tacking on logs including the error itself. But
|
||||
the logs are only included for errors that occur within the borgmatic `create`
|
||||
action (and not other actions).
|
||||
|
||||
Note that borgmatic sends logs to Healthchecks by applying the maximum of any
|
||||
other borgmatic verbosity level (`--verbosity`, `--syslog-verbosity`, etc.),
|
||||
as there is not currently a dedicated Healthchecks verbosity setting.
|
||||
|
||||
You can configure Healthchecks to notify you by a [variety of
|
||||
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
||||
|
||||
@@ -36,11 +36,12 @@ you're using Python 3, as borgmatic does not support Python 2.
|
||||
Along with the above process, you have several other options for installing
|
||||
borgmatic:
|
||||
|
||||
* [Docker image with scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/)
|
||||
* [Docker base image](https://hub.docker.com/r/monachus/borgmatic/)
|
||||
* [Docker image with support for scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/)
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Fedora official](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Fedora unofficial](https://copr.fedorainfracloud.org/coprs/heffer/borgmatic/)
|
||||
* [Arch Linux](https://www.archlinux.org/packages/community/any/borgmatic/)
|
||||
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.4.10'
|
||||
VERSION = '1.4.12'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -9,7 +9,9 @@ from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command):
|
||||
flexmock(module).should_receive('execute_command').with_args(command).once()
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command, error_on_warnings=True
|
||||
).once()
|
||||
|
||||
|
||||
def insert_execute_command_never():
|
||||
|
||||
@@ -115,36 +115,51 @@ def test_load_configurations_logs_critical_for_parse_error():
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_log_record_does_not_raise():
|
||||
module.log_record(levelno=1, foo='bar', baz='quux')
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_output_logs_for_message_only():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
|
||||
logs = tuple(module.make_error_log_records('Error'))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_output_logs_for_called_process_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module.logging).should_receive('makeLogRecord').replace_with(dict)
|
||||
|
||||
logs = tuple(
|
||||
module.make_error_log_records(
|
||||
'Error', subprocess.CalledProcessError(1, 'ls', 'error output')
|
||||
)
|
||||
)
|
||||
|
||||
assert {log.levelno for log in logs} == {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_make_error_log_records_generates_logs_for_value_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
|
||||
logs = tuple(module.make_error_log_records('Error', ValueError()))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_logs_for_os_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
|
||||
logs = tuple(module.make_error_log_records('Error', OSError()))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_nothing_for_other_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
|
||||
logs = tuple(module.make_error_log_records('Error', KeyError()))
|
||||
|
||||
assert logs == ()
|
||||
@@ -268,6 +283,7 @@ def test_collect_configuration_run_summary_logs_run_configuration_error():
|
||||
flexmock(module).should_receive('run_configuration').and_return(
|
||||
[logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
|
||||
)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return([])
|
||||
arguments = {}
|
||||
|
||||
logs = tuple(
|
||||
|
||||
@@ -28,9 +28,11 @@ def test_make_database_dump_filename_with_invalid_name_raises():
|
||||
|
||||
def test_flatten_dump_patterns_produces_list_of_all_patterns():
|
||||
dump_patterns = {'postgresql_databases': ['*/glob', 'glob/*'], 'mysql_databases': ['*/*/*']}
|
||||
expected_patterns = dump_patterns['postgresql_databases'] + dump_patterns['mysql_databases']
|
||||
expected_patterns = sorted(
|
||||
dump_patterns['postgresql_databases'] + dump_patterns['mysql_databases']
|
||||
)
|
||||
|
||||
assert module.flatten_dump_patterns(dump_patterns, ('bob',)) == expected_patterns
|
||||
assert sorted(module.flatten_dump_patterns(dump_patterns, ('bob',))) == expected_patterns
|
||||
|
||||
|
||||
def test_flatten_dump_patterns_with_no_patterns_errors():
|
||||
|
||||
@@ -3,38 +3,103 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks import healthchecks as module
|
||||
|
||||
|
||||
def test_forgetful_buffering_handler_emit_collects_log_records():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||
|
||||
assert handler.buffer == ['foo\n', 'bar\n']
|
||||
assert not handler.forgot
|
||||
|
||||
|
||||
def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reached():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=len('foo\nbar\n'))
|
||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||
assert handler.buffer == ['foo\n']
|
||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||
assert handler.buffer == ['foo\n', 'bar\n']
|
||||
handler.emit(flexmock(getMessage=lambda: 'baz'))
|
||||
assert handler.buffer == ['bar\n', 'baz\n']
|
||||
handler.emit(flexmock(getMessage=lambda: 'quux'))
|
||||
assert handler.buffer == ['quux\n']
|
||||
assert handler.forgot
|
||||
|
||||
|
||||
def test_format_buffered_logs_for_payload_flattens_log_buffer():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler.buffer = ['foo\n', 'bar\n']
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
|
||||
|
||||
payload = module.format_buffered_logs_for_payload()
|
||||
|
||||
assert payload == 'foo\nbar\n'
|
||||
|
||||
|
||||
def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs_forgotten():
|
||||
handler = module.Forgetful_buffering_handler(byte_capacity=100)
|
||||
handler.buffer = ['foo\n', 'bar\n']
|
||||
handler.forgot = True
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
|
||||
|
||||
payload = module.format_buffered_logs_for_payload()
|
||||
|
||||
assert payload == '...\nfoo\nbar\n'
|
||||
|
||||
|
||||
def test_format_buffered_logs_for_payload_without_handler_produces_empty_payload():
|
||||
flexmock(module.logging).should_receive('getLogger').and_return(
|
||||
flexmock(handlers=[module.logging.Handler()])
|
||||
)
|
||||
|
||||
payload = module.format_buffered_logs_for_payload()
|
||||
|
||||
assert payload == ''
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'start'))
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'{}/{}'.format(ping_url, 'start'), data=''
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args(ping_url)
|
||||
payload = flexmock()
|
||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(ping_url, data=payload)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.FINISH, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'fail'))
|
||||
payload = flexmock()
|
||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'{}/{}'.format(ping_url, 'fail'), data=payload
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.FAIL, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_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)
|
||||
payload = flexmock()
|
||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'https://hc-ping.com/{}'.format(ping_uuid), data=payload
|
||||
)
|
||||
|
||||
module.ping_monitor(ping_uuid, 'config.yaml', state=module.monitor.State.FINISH, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
flexmock(module.requests).should_receive('post').never()
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=True)
|
||||
|
||||