Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab9daaa0f | ||
|
|
0c6c61a272 | ||
|
|
00f62ca023 | ||
|
|
9b2ca15de6 | ||
|
|
c4aa34bf5c | ||
|
|
4385f2a36a | ||
|
|
ed6a9dadf8 | ||
|
|
d978a2d190 | ||
|
|
375036e409 | ||
|
|
99168c1035 | ||
|
|
f4a231420f | ||
|
|
55ebfdda39 | ||
|
|
e63e2e0852 | ||
|
|
edc4b9e60e | ||
|
|
78ff734e6c | ||
|
|
2cc743cf47 | ||
|
|
d99e6d1994 | ||
|
|
50f62d73b7 | ||
|
|
26a89de790 | ||
|
|
c2276b18c5 | ||
|
|
693434f8aa | ||
|
|
1e8edc05e9 | ||
|
|
1f166a47e9 | ||
|
|
9ee6151999 | ||
|
|
6cdc92bd0c | ||
|
|
612e1fea67 | ||
|
|
0a9f4e8708 | ||
|
|
781fac3266 | ||
|
|
4c38810a32 |
@@ -32,6 +32,8 @@ module.exports = function(eleventyConfig) {
|
||||
.use(markdownItReplaceLink)
|
||||
);
|
||||
|
||||
eleventyConfig.addPassthroughCopy({"docs/static": "static"});
|
||||
|
||||
return {
|
||||
templateFormats: [
|
||||
"md",
|
||||
|
||||
@@ -28,4 +28,8 @@ Use `sudo borg --version`
|
||||
|
||||
Use `python3 --version`
|
||||
|
||||
**Database version (if applicable):** [version here]
|
||||
|
||||
Use `psql --version` or `mysql --version` on client and server.
|
||||
|
||||
**operating system and version:** [OS here]
|
||||
|
||||
@@ -11,4 +11,4 @@ Robin `ypid` Schneider: Support additional options of Borg and add validate-borg
|
||||
Scott Squires: Custom archive names
|
||||
Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option
|
||||
|
||||
Any many others! See the output of "git log".
|
||||
And many others! See the output of "git log".
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
1.4.17
|
||||
* #235: Pass extra options directly to particular Borg commands, handy for Borg options that
|
||||
borgmatic does not yet support natively. Use "extra_borg_options" in the storage configuration
|
||||
section.
|
||||
* #266: Attempt to repair any inconsistencies found during a consistency check via
|
||||
"borgmatic check --repair" flag.
|
||||
|
||||
1.4.16
|
||||
* #256: Fix for "before_backup" hook not triggering an error when the command contains "borg" and
|
||||
has an exit code of 1.
|
||||
* #257: Fix for garbled Borg file listing when using "borgmatic create --progress" with
|
||||
verbosity level 1 or 2.
|
||||
* #260: Fix for missing Healthchecks monitoring payload or HTTP 500 due to incorrect unicode
|
||||
encoding.
|
||||
|
||||
1.4.15
|
||||
* Fix for database dump removal incorrectly skipping some database dumps.
|
||||
* #123: Support for mounting an archive as a FUSE filesystem via "borgmatic mount" action, and
|
||||
unmounting via "borgmatic umount". See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#mount-a-filesystem
|
||||
|
||||
1.4.14
|
||||
* Show summary log errors regardless of verbosity level, and log the "summary:" header with a log
|
||||
level based on the contained summary logs.
|
||||
|
||||
1.4.13
|
||||
* Show full error logs at "--verbosity 0" so you can see command output without upping the
|
||||
verbosity level.
|
||||
|
||||
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
|
||||
lead to data loss, please upgrade to get the fix before using "borgmatic restore".
|
||||
* Reopen the file given by "--log-file" flag if an external program rotates the log file while
|
||||
borgmatic is running.
|
||||
|
||||
1.4.9
|
||||
* #228: Database dump hooks for MySQL/MariaDB, so you can easily dump your databases before backups
|
||||
run.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.borg import extract
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
DEFAULT_CHECKS = ('repository', 'archives')
|
||||
DEFAULT_PREFIX = '{hostname}-'
|
||||
@@ -91,23 +91,23 @@ def check_archives(
|
||||
consistency_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
repair=None,
|
||||
only_checks=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a storage config dict, a consistency config dict,
|
||||
local/remote commands to run, and an optional list of checks to use instead of configured
|
||||
checks, check the contained Borg archives for consistency.
|
||||
local/remote commands to run, whether to attempt a repair, and an optional list of checks
|
||||
to use instead of configured checks, check the contained Borg archives for consistency.
|
||||
|
||||
If there are no consistency checks to run, skip running them.
|
||||
'''
|
||||
checks = _parse_checks(consistency_config, only_checks)
|
||||
check_last = consistency_config.get('check_last', None)
|
||||
lock_wait = None
|
||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('check', '')
|
||||
|
||||
if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))):
|
||||
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
|
||||
|
||||
verbosity_flags = ()
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
@@ -119,14 +119,22 @@ def check_archives(
|
||||
|
||||
full_command = (
|
||||
(local_path, 'check')
|
||||
+ (('--repair',) if repair else ())
|
||||
+ _make_check_flags(checks, check_last, prefix)
|
||||
+ remote_path_flags
|
||||
+ lock_wait_flags
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ verbosity_flags
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command)
|
||||
# The Borg repair option trigger an interactive prompt, which won't work when output is
|
||||
# captured.
|
||||
if repair:
|
||||
execute_command_without_capture(full_command, error_on_warnings=True)
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
@@ -150,6 +150,7 @@ def create_archive(
|
||||
files_cache = location_config.get('files_cache')
|
||||
default_archive_name_format = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
|
||||
archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
|
||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
|
||||
|
||||
full_command = (
|
||||
(local_path, 'create')
|
||||
@@ -170,7 +171,11 @@ def create_archive(
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) and not json else ())
|
||||
+ (
|
||||
('--list', '--filter', 'AME-')
|
||||
if logger.isEnabledFor(logging.INFO) and not json and not progress
|
||||
else ()
|
||||
)
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
||||
+ (
|
||||
('--stats',)
|
||||
@@ -181,6 +186,7 @@ def create_archive(
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (('--json',) if json else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (
|
||||
'{repository}::{archive_name_format}'.format(
|
||||
repository=repository, archive_name_format=archive_name_format
|
||||
@@ -192,7 +198,7 @@ def create_archive(
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
execute_command_without_capture(full_command, error_on_warnings=False)
|
||||
return
|
||||
|
||||
if json:
|
||||
@@ -202,4 +208,4 @@ def create_archive(
|
||||
else:
|
||||
output_log_level = logging.INFO
|
||||
|
||||
return execute_command(full_command, output_log_level)
|
||||
return execute_command(full_command, output_log_level, error_on_warnings=False)
|
||||
|
||||
@@ -27,7 +27,7 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
list_output = execute_command(full_list_command, output_log_level=None)
|
||||
list_output = execute_command(full_list_command, output_log_level=None, error_on_warnings=False)
|
||||
|
||||
try:
|
||||
last_archive_name = list_output.strip().splitlines()[-1]
|
||||
|
||||
@@ -39,5 +39,7 @@ def display_archives_info(
|
||||
)
|
||||
|
||||
return execute_command(
|
||||
full_command, output_log_level=None if info_arguments.json else logging.WARNING
|
||||
full_command,
|
||||
output_log_level=None if info_arguments.json else logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
|
||||
|
||||
def initialize_repository(
|
||||
repository,
|
||||
storage_config,
|
||||
encryption_mode,
|
||||
append_only=None,
|
||||
storage_quota=None,
|
||||
@@ -18,9 +19,9 @@ def initialize_repository(
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a Borg encryption mode, whether the repository should
|
||||
be append-only, and the storage quota to use, initialize the repository. If the repository
|
||||
already exists, then log and skip initialization.
|
||||
Given a local or remote repository path, a storage configuration dict, a Borg encryption mode,
|
||||
whether the repository should be append-only, and the storage quota to use, initialize the
|
||||
repository. If the repository already exists, then log and skip initialization.
|
||||
'''
|
||||
info_command = (local_path, 'info', repository)
|
||||
logger.debug(' '.join(info_command))
|
||||
@@ -33,6 +34,8 @@ def initialize_repository(
|
||||
if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
|
||||
raise
|
||||
|
||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('init', '')
|
||||
|
||||
init_command = (
|
||||
(local_path, 'init')
|
||||
+ (('--encryption', encryption_mode) if encryption_mode else ())
|
||||
@@ -41,8 +44,9 @@ def initialize_repository(
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
# Don't use execute_command() here because it doesn't support interactive prompts.
|
||||
execute_command_without_capture(init_command)
|
||||
execute_command_without_capture(init_command, error_on_warnings=False)
|
||||
|
||||
@@ -46,5 +46,7 @@ def list_archives(repository, storage_config, list_arguments, local_path='borg',
|
||||
)
|
||||
|
||||
return execute_command(
|
||||
full_command, output_log_level=None if list_arguments.json else logging.WARNING
|
||||
full_command,
|
||||
output_log_level=None if list_arguments.json else logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mount_archive(
|
||||
repository,
|
||||
archive,
|
||||
mount_point,
|
||||
paths,
|
||||
foreground,
|
||||
options,
|
||||
storage_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, an archive name, a filesystem mount point, zero or more
|
||||
paths to mount from the archive, extra Borg mount options, a storage configuration dict, and
|
||||
optional local and remote Borg paths, mount the archive onto the mount point.
|
||||
'''
|
||||
umask = storage_config.get('umask', None)
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
(local_path, 'mount')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--foreground',) if foreground else ())
|
||||
+ (('-o', options) if options else ())
|
||||
+ ('::'.join((repository, archive)),)
|
||||
+ (mount_point,)
|
||||
+ (tuple(paths) if paths else ())
|
||||
)
|
||||
|
||||
# Don't capture the output when foreground mode is used so that ctrl-C can work properly.
|
||||
if foreground:
|
||||
execute_command_without_capture(full_command, error_on_warnings=False)
|
||||
return
|
||||
|
||||
execute_command(full_command, error_on_warnings=False)
|
||||
@@ -49,6 +49,7 @@ def prune_archives(
|
||||
'''
|
||||
umask = storage_config.get('umask', None)
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
extra_borg_options = storage_config.get('extra_borg_options', {}).get('prune', '')
|
||||
|
||||
full_command = (
|
||||
(local_path, 'prune')
|
||||
@@ -61,7 +62,12 @@ def prune_archives(
|
||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--stats',) if stats else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
|
||||
execute_command(
|
||||
full_command,
|
||||
output_log_level=logging.WARNING if stats else logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def unmount_archive(mount_point, local_path='borg'):
|
||||
'''
|
||||
Given a mounted filesystem mount point, and an optional local Borg paths, umount the filesystem
|
||||
from the mount point.
|
||||
'''
|
||||
full_command = (
|
||||
(local_path, 'umount')
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (mount_point,)
|
||||
)
|
||||
|
||||
execute_command(full_command, error_on_warnings=True)
|
||||
@@ -9,6 +9,8 @@ SUBPARSER_ALIASES = {
|
||||
'create': ['--create', '-C'],
|
||||
'check': ['--check', '-k'],
|
||||
'extract': ['--extract', '-x'],
|
||||
'mount': ['--mount', '-m'],
|
||||
'umount': ['--umount', '-u'],
|
||||
'restore': ['--restore', '-r'],
|
||||
'list': ['--list', '-l'],
|
||||
'info': ['--info', '-i'],
|
||||
@@ -140,21 +142,21 @@ def parse_arguments(*unparsed_arguments):
|
||||
type=int,
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Display verbose progress to the console (from none to lots: 0, 1, or 2) or only errors (-1)',
|
||||
help='Display verbose progress to the console (from only errors to very verbose: -1, 0, 1, or 2)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--syslog-verbosity',
|
||||
type=int,
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to syslog (from none to lots: 0, 1, or 2) or only errors (-1). Ignored when console is interactive or --log-file is given',
|
||||
help='Log verbose progress to syslog (from only errors to very verbose: -1, 0, 1, or 2). Ignored when console is interactive or --log-file is given',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file-verbosity',
|
||||
type=int,
|
||||
choices=range(-1, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to log file (from none to lots: 0, 1, or 2) or only errors (-1). Only used when --log-file is given',
|
||||
help='Log verbose progress to log file (from only errors to very verbose: -1, 0, 1, or 2). Only used when --log-file is given',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file',
|
||||
@@ -172,9 +174,9 @@ def parse_arguments(*unparsed_arguments):
|
||||
|
||||
top_level_parser = ArgumentParser(
|
||||
description='''
|
||||
A simple wrapper script for the Borg backup software that creates and prunes backups.
|
||||
If none of the action options are given, then borgmatic defaults to: prune, create, and
|
||||
check archives.
|
||||
Simple, configuration-driven backup software for servers and workstations. If none of
|
||||
the action options are given, then borgmatic defaults to: prune, create, and check
|
||||
archives.
|
||||
''',
|
||||
parents=[global_parser],
|
||||
)
|
||||
@@ -264,6 +266,13 @@ def parse_arguments(*unparsed_arguments):
|
||||
add_help=False,
|
||||
)
|
||||
check_group = check_parser.add_argument_group('check arguments')
|
||||
check_group.add_argument(
|
||||
'--repair',
|
||||
dest='repair',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Attempt to repair any inconsistencies found (experimental and only for interactive use)',
|
||||
)
|
||||
check_group.add_argument(
|
||||
'--only',
|
||||
metavar='CHECK',
|
||||
@@ -312,6 +321,60 @@ def parse_arguments(*unparsed_arguments):
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
mount_parser = subparsers.add_parser(
|
||||
'mount',
|
||||
aliases=SUBPARSER_ALIASES['mount'],
|
||||
help='Mount files from a named archive as a FUSE filesystem',
|
||||
description='Mount a named archive as a FUSE filesystem',
|
||||
add_help=False,
|
||||
)
|
||||
mount_group = mount_parser.add_argument_group('mount arguments')
|
||||
mount_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
||||
)
|
||||
mount_group.add_argument('--archive', help='Name of archive to mount', required=True)
|
||||
mount_group.add_argument(
|
||||
'--mount-point',
|
||||
metavar='PATH',
|
||||
dest='mount_point',
|
||||
help='Path where filesystem is to be mounted',
|
||||
required=True,
|
||||
)
|
||||
mount_group.add_argument(
|
||||
'--path',
|
||||
metavar='PATH',
|
||||
nargs='+',
|
||||
dest='paths',
|
||||
help='Paths to mount from archive, defaults to the entire archive',
|
||||
)
|
||||
mount_group.add_argument(
|
||||
'--foreground',
|
||||
dest='foreground',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Stay in foreground until ctrl-C is pressed',
|
||||
)
|
||||
mount_group.add_argument('--options', dest='options', help='Extra Borg mount options')
|
||||
mount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
umount_parser = subparsers.add_parser(
|
||||
'umount',
|
||||
aliases=SUBPARSER_ALIASES['umount'],
|
||||
help='Unmount a FUSE filesystem that was mounted with "borgmatic mount"',
|
||||
description='Unmount a mounted FUSE filesystem',
|
||||
add_help=False,
|
||||
)
|
||||
umount_group = umount_parser.add_argument_group('umount arguments')
|
||||
umount_group.add_argument(
|
||||
'--mount-point',
|
||||
metavar='PATH',
|
||||
dest='mount_point',
|
||||
help='Path of filesystem to unmount',
|
||||
required=True,
|
||||
)
|
||||
umount_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
restore_parser = subparsers.add_parser(
|
||||
'restore',
|
||||
aliases=SUBPARSER_ALIASES['restore'],
|
||||
|
||||
@@ -15,10 +15,12 @@ from borgmatic.borg import extract as borg_extract
|
||||
from borgmatic.borg import info as borg_info
|
||||
from borgmatic.borg import init as borg_init
|
||||
from borgmatic.borg import list as borg_list
|
||||
from borgmatic.borg import mount as borg_mount
|
||||
from borgmatic.borg import prune as borg_prune
|
||||
from borgmatic.borg import umount as borg_umount
|
||||
from borgmatic.commands.arguments import parse_arguments
|
||||
from borgmatic.config import checks, collect, convert, validate
|
||||
from borgmatic.hooks import command, cronhub, cronitor, dispatch, dump, healthchecks
|
||||
from borgmatic.hooks import command, dispatch, dump, monitor
|
||||
from borgmatic.logger import configure_logging, should_do_markup
|
||||
from borgmatic.signals import configure_signals
|
||||
from borgmatic.verbosity import verbosity_to_log_level
|
||||
@@ -53,14 +55,13 @@ def run_configuration(config_filename, config, arguments):
|
||||
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'run'
|
||||
)
|
||||
cronhub.ping_cronhub(
|
||||
hooks.get('cronhub'), config_filename, global_arguments.dry_run, 'start'
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
hooks,
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.START,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
command.execute_hook(
|
||||
hooks.get('before_backup'),
|
||||
@@ -119,14 +120,13 @@ def run_configuration(config_filename, config, arguments):
|
||||
'post-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'complete'
|
||||
)
|
||||
cronhub.ping_cronhub(
|
||||
hooks.get('cronhub'), config_filename, global_arguments.dry_run, 'finish'
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
hooks,
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.FINISH,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = error
|
||||
@@ -146,14 +146,13 @@ def run_configuration(config_filename, config, arguments):
|
||||
error=encountered_error,
|
||||
output=getattr(encountered_error, 'output', ''),
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
||||
)
|
||||
cronitor.ping_cronitor(
|
||||
hooks.get('cronitor'), config_filename, global_arguments.dry_run, 'fail'
|
||||
)
|
||||
cronhub.ping_cronhub(
|
||||
hooks.get('cronhub'), config_filename, global_arguments.dry_run, 'fail'
|
||||
dispatch.call_hooks(
|
||||
'ping_monitor',
|
||||
hooks,
|
||||
config_filename,
|
||||
monitor.MONITOR_HOOK_NAMES,
|
||||
monitor.State.FAIL,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
yield from make_error_log_records(
|
||||
@@ -190,6 +189,7 @@ def run_actions(
|
||||
logger.info('{}: Initializing repository'.format(repository))
|
||||
borg_init.initialize_repository(
|
||||
repository,
|
||||
storage,
|
||||
arguments['init'].encryption_mode,
|
||||
arguments['init'].append_only,
|
||||
arguments['init'].storage_quota,
|
||||
@@ -230,6 +230,7 @@ def run_actions(
|
||||
consistency,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
repair=arguments['check'].repair,
|
||||
only_checks=arguments['check'].only,
|
||||
)
|
||||
if 'extract' in arguments:
|
||||
@@ -249,6 +250,27 @@ def run_actions(
|
||||
destination_path=arguments['extract'].destination,
|
||||
progress=arguments['extract'].progress,
|
||||
)
|
||||
if 'mount' in arguments:
|
||||
if arguments['mount'].repository is None or repository == arguments['mount'].repository:
|
||||
logger.info('{}: Mounting archive {}'.format(repository, arguments['mount'].archive))
|
||||
borg_mount.mount_archive(
|
||||
repository,
|
||||
arguments['mount'].archive,
|
||||
arguments['mount'].mount_point,
|
||||
arguments['mount'].paths,
|
||||
arguments['mount'].foreground,
|
||||
arguments['mount'].options,
|
||||
storage,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
if 'umount' in arguments:
|
||||
logger.info(
|
||||
'{}: Unmounting mount point {}'.format(repository, arguments['umount'].mount_point)
|
||||
)
|
||||
borg_umount.unmount_archive(
|
||||
mount_point=arguments['umount'].mount_point, local_path=local_path
|
||||
)
|
||||
if 'restore' in arguments:
|
||||
if arguments['restore'].repository is None or repository == arguments['restore'].repository:
|
||||
logger.info(
|
||||
@@ -269,12 +291,13 @@ def run_actions(
|
||||
dump.DATABASE_HOOK_NAMES,
|
||||
restore_names,
|
||||
)
|
||||
|
||||
borg_extract.extract_archive(
|
||||
global_arguments.dry_run,
|
||||
repository,
|
||||
arguments['restore'].archive,
|
||||
dump.convert_glob_patterns_to_borg_patterns(
|
||||
[pattern for patterns in dump_patterns.values() for pattern in patterns]
|
||||
dump.flatten_dump_patterns(dump_patterns, restore_names)
|
||||
),
|
||||
location,
|
||||
storage,
|
||||
@@ -367,33 +390,41 @@ def load_configurations(config_filenames):
|
||||
return (configs, logs)
|
||||
|
||||
|
||||
def log_record(suppress_log=False, **kwargs):
|
||||
'''
|
||||
Create a log record based on the given makeLogRecord() arguments, one of which must be
|
||||
named "levelno". Log the record (unless suppress log is set) and return it.
|
||||
'''
|
||||
record = logging.makeLogRecord(kwargs)
|
||||
if suppress_log:
|
||||
return record
|
||||
|
||||
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)
|
||||
# Suppress these logs for now and save full error output for the log summary at the end.
|
||||
yield log_record(
|
||||
levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output, suppress_log=True
|
||||
)
|
||||
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.
|
||||
@@ -415,6 +446,8 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
||||
repository = arguments['extract'].repository
|
||||
elif 'list' in arguments and arguments['list'].archive:
|
||||
repository = arguments['list'].repository
|
||||
elif 'mount' in arguments:
|
||||
repository = arguments['mount'].repository
|
||||
else:
|
||||
repository = None
|
||||
|
||||
@@ -535,15 +568,18 @@ def main(): # pragma: no cover
|
||||
logger.debug('Ensuring legacy configuration is upgraded')
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||
|
||||
summary_logs = list(collect_configuration_run_summary_logs(configs, arguments))
|
||||
summary_logs = parse_logs + list(collect_configuration_run_summary_logs(configs, arguments))
|
||||
summary_logs_max_level = max(log.levelno for log in summary_logs)
|
||||
|
||||
logger.info('')
|
||||
logger.info('summary:')
|
||||
[
|
||||
for message in ('', 'summary:'):
|
||||
log_record(
|
||||
levelno=summary_logs_max_level,
|
||||
levelname=logging.getLevelName(summary_logs_max_level),
|
||||
msg=message,
|
||||
)
|
||||
|
||||
for log in summary_logs:
|
||||
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):
|
||||
if summary_logs_max_level >= logging.CRITICAL:
|
||||
exit_with_help_link()
|
||||
|
||||
@@ -245,6 +245,29 @@ map:
|
||||
Bypass Borg error about a previously unknown unencrypted repository. Defaults to
|
||||
false.
|
||||
example: true
|
||||
extra_borg_options:
|
||||
map:
|
||||
init:
|
||||
type: str
|
||||
desc: Extra command-line options to pass to "borg init".
|
||||
example: "--make-parent-dirs"
|
||||
prune:
|
||||
type: str
|
||||
desc: Extra command-line options to pass to "borg prune".
|
||||
example: "--save-space"
|
||||
create:
|
||||
type: str
|
||||
desc: Extra command-line options to pass to "borg create".
|
||||
example: "--no-files-cache"
|
||||
check:
|
||||
type: str
|
||||
desc: Extra command-line options to pass to "borg check".
|
||||
example: "--save-space"
|
||||
desc: |
|
||||
Additional options to pass directly to particular Borg commands, handy for Borg
|
||||
options that borgmatic does not yet support natively. Note that borgmatic does
|
||||
not perform any validation on these options. Running borgmatic with
|
||||
"--verbosity 2" shows the exact Borg command-line invocation.
|
||||
retention:
|
||||
desc: |
|
||||
Retention policy for how many backups to keep in each category. See
|
||||
|
||||
@@ -9,15 +9,15 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
||||
BORG_ERROR_EXIT_CODE = 2
|
||||
|
||||
|
||||
def exit_code_indicates_error(command, exit_code, error_on_warnings=False):
|
||||
def exit_code_indicates_error(command, exit_code, error_on_warnings=True):
|
||||
'''
|
||||
Return True if the given exit code from running the command corresponds to an error.
|
||||
If error on warnings is False, then treat exit code 1 as a warning instead of an error.
|
||||
'''
|
||||
# If we're running something other than Borg, treat all non-zero exit codes as errors.
|
||||
if 'borg' in command[0] and not error_on_warnings:
|
||||
return bool(exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
if error_on_warnings:
|
||||
return bool(exit_code != 0)
|
||||
|
||||
return bool(exit_code != 0)
|
||||
return bool(exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
|
||||
|
||||
def log_output(command, process, output_buffer, output_log_level, error_on_warnings):
|
||||
@@ -65,7 +65,7 @@ def execute_command(
|
||||
shell=False,
|
||||
extra_environment=None,
|
||||
working_directory=None,
|
||||
error_on_warnings=False,
|
||||
error_on_warnings=True,
|
||||
):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings) and log its output at the
|
||||
@@ -75,7 +75,7 @@ def execute_command(
|
||||
file. If shell is True, execute the command within a shell. If an extra environment dict is
|
||||
given, then use it to augment the current environment, and pass the result into the command. If
|
||||
a working directory is given, use that as the present working directory when running the
|
||||
command.
|
||||
command. If error on warnings is False, then treat exit code 1 as a warning instead of an error.
|
||||
|
||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||
'''
|
||||
@@ -110,14 +110,14 @@ def execute_command(
|
||||
)
|
||||
|
||||
|
||||
def execute_command_without_capture(full_command, working_directory=None, error_on_warnings=False):
|
||||
def execute_command_without_capture(full_command, working_directory=None, error_on_warnings=True):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings), but don't capture or log its
|
||||
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
|
||||
display) or provide interactive prompts.
|
||||
|
||||
If a working directory is given, use that as the present working directory when running the
|
||||
command.
|
||||
command. If error on warnings is False, then treat exit code 1 as a warning instead of an error.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
|
||||
|
||||
@@ -2,23 +2,29 @@ import logging
|
||||
|
||||
import requests
|
||||
|
||||
from borgmatic.hooks import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MONITOR_STATE_TO_CRONHUB = {
|
||||
monitor.State.START: 'start',
|
||||
monitor.State.FINISH: 'finish',
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
|
||||
def ping_cronhub(ping_url, config_filename, dry_run, state):
|
||||
|
||||
def ping_monitor(ping_url, config_filename, state, dry_run):
|
||||
'''
|
||||
Ping the given Cronhub URL, substituting in the state string. Use the given configuration
|
||||
Ping the given Cronhub URL, modified with the monitor.State. Use the given configuration
|
||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
if not ping_url:
|
||||
logger.debug('{}: No Cronhub hook set'.format(config_filename))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
formatted_state = '/{}/'.format(state)
|
||||
formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
|
||||
ping_url = ping_url.replace('/start/', formatted_state).replace('/ping/', formatted_state)
|
||||
|
||||
logger.info('{}: Pinging Cronhub {}{}'.format(config_filename, state, dry_run_label))
|
||||
logger.info(
|
||||
'{}: Pinging Cronhub {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
logger.debug('{}: Using Cronhub ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
if not dry_run:
|
||||
|
||||
@@ -2,22 +2,28 @@ import logging
|
||||
|
||||
import requests
|
||||
|
||||
from borgmatic.hooks import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MONITOR_STATE_TO_CRONITOR = {
|
||||
monitor.State.START: 'run',
|
||||
monitor.State.FINISH: 'complete',
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
|
||||
def ping_cronitor(ping_url, config_filename, dry_run, append):
|
||||
'''
|
||||
Ping the given Cronitor URL, appending the append string. Use the given configuration filename
|
||||
in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
if not ping_url:
|
||||
logger.debug('{}: No Cronitor hook set'.format(config_filename))
|
||||
return
|
||||
|
||||
def ping_monitor(ping_url, config_filename, state, dry_run):
|
||||
'''
|
||||
Ping the given Cronitor URL, modified with the monitor.State. Use the given configuration
|
||||
filename in any log entries. If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
ping_url = '{}/{}'.format(ping_url, append)
|
||||
ping_url = '{}/{}'.format(ping_url, MONITOR_STATE_TO_CRONITOR[state])
|
||||
|
||||
logger.info('{}: Pinging Cronitor {}{}'.format(config_filename, append, dry_run_label))
|
||||
logger.info(
|
||||
'{}: Pinging Cronitor {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
if not dry_run:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.hooks import mysql, postgresql
|
||||
from borgmatic.hooks import cronhub, cronitor, healthchecks, mysql, postgresql
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HOOK_NAME_TO_MODULE = {'postgresql_databases': postgresql, 'mysql_databases': mysql}
|
||||
HOOK_NAME_TO_MODULE = {
|
||||
'healthchecks': healthchecks,
|
||||
'cronitor': cronitor,
|
||||
'cronhub': cronhub,
|
||||
'postgresql_databases': postgresql,
|
||||
'mysql_databases': mysql,
|
||||
}
|
||||
|
||||
|
||||
def call_hook(function_name, hooks, log_prefix, hook_name, *args, **kwargs):
|
||||
|
||||
@@ -20,6 +20,26 @@ def make_database_dump_filename(dump_path, name, hostname=None):
|
||||
return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
|
||||
|
||||
|
||||
def flatten_dump_patterns(dump_patterns, names):
|
||||
'''
|
||||
Given a dict from a database hook name to glob patterns matching the dumps for the named
|
||||
databases, flatten out all the glob patterns into a single sequence, and return it.
|
||||
|
||||
Raise ValueError if there are no resulting glob patterns, which indicates that databases are not
|
||||
configured in borgmatic's configuration.
|
||||
'''
|
||||
flattened = [pattern for patterns in dump_patterns.values() for pattern in patterns]
|
||||
|
||||
if not flattened:
|
||||
raise ValueError(
|
||||
'Cannot restore database(s) {} missing from borgmatic\'s configuration'.format(
|
||||
', '.join(names) or '"all"'
|
||||
)
|
||||
)
|
||||
|
||||
return flattened
|
||||
|
||||
|
||||
def remove_database_dumps(dump_path, databases, database_type_name, log_prefix, dry_run):
|
||||
'''
|
||||
Remove the database dumps for the given databases in the dump directory path. The databases are
|
||||
@@ -51,10 +71,10 @@ def remove_database_dumps(dump_path, databases, database_type_name, log_prefix,
|
||||
continue
|
||||
|
||||
os.remove(dump_filename)
|
||||
dump_path = os.path.dirname(dump_filename)
|
||||
dump_file_dir = os.path.dirname(dump_filename)
|
||||
|
||||
if len(os.listdir(dump_path)) == 0:
|
||||
os.rmdir(dump_path)
|
||||
if len(os.listdir(dump_file_dir)) == 0:
|
||||
os.rmdir(dump_file_dir)
|
||||
|
||||
|
||||
def convert_glob_patterns_to_borg_patterns(patterns):
|
||||
@@ -109,7 +129,6 @@ def get_per_hook_database_configurations(hooks, names, dump_patterns):
|
||||
Raise ValueError if one of the database names cannot be matched to a database in borgmatic's
|
||||
database configuration.
|
||||
'''
|
||||
# TODO: Need to filter names by database type? Maybe take a database --type argument to disambiguate.
|
||||
hook_databases = {
|
||||
hook_name: list(
|
||||
get_database_configurations(
|
||||
@@ -122,6 +141,11 @@ def get_per_hook_database_configurations(hooks, names, dump_patterns):
|
||||
}
|
||||
|
||||
if not names or 'all' in names:
|
||||
if not any(hook_databases.values()):
|
||||
raise ValueError(
|
||||
'Cannot restore database "all", as there are no database dumps in the archive'
|
||||
)
|
||||
|
||||
return hook_databases
|
||||
|
||||
found_names = {
|
||||
|
||||
@@ -2,18 +2,79 @@ import logging
|
||||
|
||||
import requests
|
||||
|
||||
from borgmatic.hooks import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MONITOR_STATE_TO_HEALTHCHECKS = {
|
||||
monitor.State.START: 'start',
|
||||
monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
|
||||
monitor.State.FAIL: 'fail',
|
||||
}
|
||||
|
||||
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
||||
PAYLOAD_LIMIT_BYTES = 10 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR)
|
||||
|
||||
|
||||
class Forgetful_buffering_handler(logging.Handler):
|
||||
'''
|
||||
Ping the given Healthchecks URL or UUID, appending the append string if any. Use the given
|
||||
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):
|
||||
'''
|
||||
Ping the given Healthchecks URL or UUID, modified with the monitor.State. Use the given
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||
anything.
|
||||
'''
|
||||
if not ping_url_or_uuid:
|
||||
logger.debug('{}: No Healthchecks hook set'.format(config_filename))
|
||||
return
|
||||
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
|
||||
@@ -22,16 +83,18 @@ def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||
)
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
||||
if append:
|
||||
ping_url = '{}/{}'.format(ping_url, append)
|
||||
healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
|
||||
if healthchecks_state:
|
||||
ping_url = '{}/{}'.format(ping_url, healthchecks_state)
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging Healthchecks{}{}'.format(
|
||||
config_filename, ' ' + append if append else '', dry_run_label
|
||||
)
|
||||
'{}: Pinging Healthchecks {}{}'.format(config_filename, state.name.lower(), dry_run_label)
|
||||
)
|
||||
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.encode('utf-8'))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from enum import Enum
|
||||
|
||||
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub')
|
||||
|
||||
|
||||
class State(Enum):
|
||||
START = 1
|
||||
FINISH = 2
|
||||
FAIL = 3
|
||||
@@ -104,7 +104,7 @@ def configure_logging(
|
||||
syslog_handler.setLevel(syslog_log_level)
|
||||
handlers = (console_handler, syslog_handler)
|
||||
elif log_file:
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
||||
file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s'))
|
||||
file_handler.setLevel(log_file_log_level)
|
||||
handlers = (console_handler, file_handler)
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM python:3.7.4-alpine3.10 as borgmatic
|
||||
COPY . /app
|
||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
RUN borgmatic --help > /command-line.txt \
|
||||
&& for action in init prune create check extract restore list info; do \
|
||||
&& for action in init prune create check extract mount umount restore list info; do \
|
||||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -87,6 +87,35 @@ so that you can extract files from your archive without impacting your live
|
||||
databases.
|
||||
|
||||
|
||||
## Mount a filesystem
|
||||
|
||||
If instead of extracting files, you'd like to explore the files from an
|
||||
archive as a [FUSE](https://en.wikipedia.org/wiki/Filesystem_in_Userspace)
|
||||
filesystem, you can use the `borgmatic mount` action. Here's an example:
|
||||
|
||||
```bash
|
||||
borgmatic mount --archive host-2019-... --mount-point /mnt
|
||||
```
|
||||
|
||||
This mounts the entire archive on the given mount point `/mnt`, so that you
|
||||
can look in there for your files.
|
||||
|
||||
If you'd like to restrict the mounted filesystem to only particular paths from
|
||||
your archive, use the `--path` flag, similar to the `extract` action above.
|
||||
For instance:
|
||||
|
||||
```bash
|
||||
borgmatic mount --archive host-2019-... --mount-point /mnt --path var/lib
|
||||
```
|
||||
|
||||
When you're all done exploring your files, unmount your mount point. No
|
||||
`--archive` flag is needed:
|
||||
|
||||
```bash
|
||||
borgmatic umount --mount-point /mnt
|
||||
```
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
||||
@@ -70,6 +70,21 @@ Or to increase syslog logging to include debug spew:
|
||||
borgmatic --syslog-verbosity 2
|
||||
```
|
||||
|
||||
### Rate limiting
|
||||
|
||||
If you are using rsyslog or systemd's journal, be aware that by default they
|
||||
both throttle the rate at which logging occurs. So you may need to change
|
||||
either [the global rate
|
||||
limit](https://www.rootusers.com/how-to-change-log-rate-limiting-in-linux/) or
|
||||
[the per-service rate
|
||||
limit](https://www.freedesktop.org/software/systemd/man/journald.conf.html#RateLimitIntervalSec=)
|
||||
if you're finding that borgmatic logs are missing.
|
||||
|
||||
Note that the [sample borgmatic systemd service
|
||||
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
|
||||
already has this rate limit disabled for systemd's journal.
|
||||
|
||||
|
||||
### Logging to file
|
||||
|
||||
If you don't want to use syslog, and you'd rather borgmatic log to a plain
|
||||
@@ -84,20 +99,6 @@ the log file so it doesn't grow too large. Also, there is a
|
||||
`--log-file-verbosity` flag to customize the log file's log level.
|
||||
|
||||
|
||||
### systemd journal
|
||||
|
||||
If your local syslog daemon is systemd's journal, be aware that journald by
|
||||
default throttles the rate at which a particular program can log. So you may
|
||||
need to [change the journald rate
|
||||
limit](https://www.freedesktop.org/software/systemd/man/journald.conf.html#RateLimitIntervalSec=)
|
||||
in `/etc/systemd/journald.conf` if you're finding that borgmatic journald logs
|
||||
are missing.
|
||||
|
||||
Note that the [sample borgmatic systemd service
|
||||
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
|
||||
already has this rate limit disabled.
|
||||
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -39,9 +39,9 @@ generate-borgmatic-config --source config.yaml --destination config-new.yaml
|
||||
New options start as commented out, so you can edit the file and decide
|
||||
whether you want to use each one.
|
||||
|
||||
There are a few caveats to this process, however. First, when generating the
|
||||
new configuration file, `generate-borgmatic-config` replaces any comments
|
||||
you've written in your original configuration file with the newest generated
|
||||
There are a few caveats to this process. First, when generating the new
|
||||
configuration file, `generate-borgmatic-config` replaces any comments you've
|
||||
written in your original configuration file with the newest generated
|
||||
comments. Second, the script adds back any options you had originally deleted,
|
||||
although it does so with the options commented out. And finally, any YAML
|
||||
includes you've used in the source configuration get flattened out into a
|
||||
|
||||
|
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.9'
|
||||
VERSION = '1.4.17'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -256,7 +256,7 @@ def test_parse_arguments_disallows_glob_archives_with_successful():
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
||||
def test_parse_arguments_disallows_repository_unless_action_consumes_it():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
@@ -271,20 +271,36 @@ def test_parse_arguments_allows_repository_with_extract():
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_allows_repository_with_mount():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments(
|
||||
'--config',
|
||||
'myconfig',
|
||||
'mount',
|
||||
'--repository',
|
||||
'test.borg',
|
||||
'--archive',
|
||||
'test',
|
||||
'--mount-point',
|
||||
'/mnt',
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_allows_repository_with_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--config', 'myconfig', 'list', '--repository', 'test.borg')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_archive_without_extract_restore_or_list():
|
||||
def test_parse_arguments_disallows_archive_unless_action_consumes_it():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_paths_without_extract():
|
||||
def test_parse_arguments_disallows_paths_unless_action_consumes_it():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
@@ -297,6 +313,14 @@ def test_parse_arguments_allows_archive_with_extract():
|
||||
module.parse_arguments('--config', 'myconfig', 'extract', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_mount():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments(
|
||||
'--config', 'myconfig', 'mount', '--archive', 'test', '--mount-point', '/mnt'
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_allows_archive_with_dashed_extract():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -328,6 +352,13 @@ def test_parse_arguments_requires_archive_with_extract():
|
||||
module.parse_arguments('--config', 'myconfig', 'extract')
|
||||
|
||||
|
||||
def test_parse_arguments_requires_archive_with_mount():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', 'mount', '--mount-point', '/mnt')
|
||||
|
||||
|
||||
def test_parse_arguments_requires_archive_with_restore():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -335,6 +366,20 @@ def test_parse_arguments_requires_archive_with_restore():
|
||||
module.parse_arguments('--config', 'myconfig', 'restore')
|
||||
|
||||
|
||||
def test_parse_arguments_requires_mount_point_with_mount():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', 'mount', '--archive', 'test')
|
||||
|
||||
|
||||
def test_parse_arguments_requires_mount_point_with_umount():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
module.parse_arguments('--config', 'myconfig', 'umount')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_progress_before_create():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
||||
@@ -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():
|
||||
@@ -156,6 +158,21 @@ def test_make_check_flags_with_default_checks_and_prefix_includes_prefix_flag():
|
||||
assert flags == ('--prefix', 'foo-')
|
||||
|
||||
|
||||
def test_check_archives_with_repair_calls_borg_with_repair_parameter():
|
||||
checks = ('repository',)
|
||||
consistency_config = {'check_last': None}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'check', '--repair', 'repo'), error_on_warnings=True
|
||||
).once()
|
||||
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={}, consistency_config=consistency_config, repair=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'checks',
|
||||
(
|
||||
@@ -294,3 +311,17 @@ def test_check_archives_with_retention_prefix():
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={}, consistency_config=consistency_config
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
checks = ('repository',)
|
||||
consistency_config = {'check_last': None}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').and_return(())
|
||||
insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository='repo',
|
||||
storage_config={'extra_borg_options': {'check': '--extra --options'}},
|
||||
consistency_config=consistency_config,
|
||||
)
|
||||
|
||||
@@ -206,7 +206,9 @@ def test_create_archive_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -232,7 +234,9 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(pattern_flags)
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -258,7 +262,9 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(exclude_flags)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -284,6 +290,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--stats') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -308,7 +315,9 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -336,6 +345,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -359,7 +369,9 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -385,7 +397,9 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -414,6 +428,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -443,6 +458,7 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -468,6 +484,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--checkpoint-interval', '600') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -492,6 +509,7 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--chunker-params', '1,2,3,4') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -516,6 +534,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--compression', 'rle') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -540,6 +559,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--remote-ratelimit', '100') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -562,7 +582,9 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -586,7 +608,9 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_paramet
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--numeric-owner') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--numeric-owner') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -610,7 +634,9 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -635,7 +661,9 @@ def test_create_archive_with_option_true_calls_borg_without_corresponding_parame
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -662,6 +690,7 @@ def test_create_archive_with_option_false_calls_borg_with_corresponding_paramete
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--no' + option_name.replace('_', '')) + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -687,6 +716,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--files-cache', 'ctime,size') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -710,7 +740,9 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg1', 'create') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -736,6 +768,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--remote-path', 'borg1') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -759,7 +792,9 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -782,7 +817,9 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -805,7 +842,9 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS, output_log_level=logging.WARNING
|
||||
('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -821,6 +860,32 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_parameter_and_no_list():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
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', '--info', '--stats', '--progress') + ARCHIVE_WITH_PATHS,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
progress=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
@@ -829,7 +894,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
|
||||
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -853,7 +918,9 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
@@ -879,7 +946,9 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=None,
|
||||
error_on_warnings=False,
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
@@ -908,6 +977,7 @@ def test_create_archive_with_source_directories_glob_expands():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
|
||||
|
||||
@@ -933,6 +1003,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'),
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
|
||||
|
||||
@@ -958,6 +1029,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -980,7 +1052,9 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), output_log_level=logging.INFO
|
||||
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'),
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -1005,6 +1079,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'),
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
@@ -1017,3 +1092,28 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||
},
|
||||
storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--extra', '--options') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={'extra_borg_options': {'create': '--extra --options'}},
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ def insert_execute_command_mock(command, working_directory=None, error_on_warnin
|
||||
|
||||
def insert_execute_command_output_mock(command, result):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command, output_log_level=None
|
||||
command, output_log_level=None, error_on_warnings=False
|
||||
).and_return(result).once()
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
|
||||
module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
|
||||
|
||||
|
||||
def test_extract_archive_calls_borg_with_restore_path_parameters():
|
||||
def test_extract_archive_calls_borg_with_path_parameters():
|
||||
flexmock(module.os.path).should_receive('abspath').and_return('repo')
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from ..test_verbosity import insert_logging_mock
|
||||
|
||||
def test_display_archives_info_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'info', 'repo'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
@@ -20,7 +20,9 @@ def test_display_archives_info_calls_borg_with_parameters():
|
||||
|
||||
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--info', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--info', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
module.display_archives_info(
|
||||
@@ -30,7 +32,7 @@ def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||
|
||||
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.INFO)
|
||||
@@ -43,7 +45,9 @@ def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_outpu
|
||||
|
||||
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--debug', '--show-rc', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--debug', '--show-rc', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -54,7 +58,7 @@ def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
||||
|
||||
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
@@ -67,7 +71,7 @@ def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_outp
|
||||
|
||||
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.display_archives_info(
|
||||
@@ -79,7 +83,7 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
||||
|
||||
def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING
|
||||
('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
@@ -89,7 +93,7 @@ def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
|
||||
|
||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'info', 'repo'), output_log_level=logging.WARNING
|
||||
('borg1', 'info', 'repo'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
@@ -102,7 +106,9 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||
|
||||
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--remote-path', 'borg1', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--remote-path', 'borg1', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
@@ -116,7 +122,9 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
|
||||
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
storage_config = {'lock_wait': 5}
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--lock-wait', '5', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--lock-wait', '5', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
@@ -131,6 +139,7 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
|
||||
@@ -24,7 +24,7 @@ def insert_info_command_not_found_mock():
|
||||
|
||||
def insert_init_command_mock(init_command, **kwargs):
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
init_command
|
||||
init_command, error_on_warnings=False
|
||||
).once()
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_initialize_repository_calls_borg_with_parameters():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('repo',))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_raises_for_borg_init_error():
|
||||
@@ -42,14 +42,16 @@ def test_initialize_repository_raises_for_borg_init_error():
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey'
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
||||
insert_info_command_found_mock()
|
||||
flexmock(module).should_receive('execute_command_without_capture').never()
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_raises_for_unknown_info_command_error():
|
||||
@@ -58,21 +60,27 @@ def test_initialize_repository_raises_for_unknown_info_command_error():
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey'
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--append-only', 'repo'))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True)
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey', append_only=True
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G', 'repo'))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G')
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey', storage_quota='5G'
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
|
||||
@@ -80,7 +88,7 @@ def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
|
||||
insert_init_command_mock(INIT_COMMAND + ('--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
|
||||
@@ -88,18 +96,33 @@ def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
|
||||
insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
module.initialize_repository(repository='repo', storage_config={}, encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_with_local_path_calls_borg_via_local_path():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:] + ('repo',))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1')
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey', local_path='borg1'
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')
|
||||
module.initialize_repository(
|
||||
repository='repo', storage_config={}, encryption_mode='repokey', remote_path='borg1'
|
||||
)
|
||||
|
||||
|
||||
def test_initialize_repository_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--extra', '--options', 'repo'))
|
||||
|
||||
module.initialize_repository(
|
||||
repository='repo',
|
||||
storage_config={'extra_borg_options': {'init': '--extra --options'}},
|
||||
encryption_mode='repokey',
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from ..test_verbosity import insert_logging_mock
|
||||
|
||||
def test_list_archives_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', 'repo'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
@@ -22,7 +22,9 @@ def test_list_archives_calls_borg_with_parameters():
|
||||
|
||||
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--info', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--info', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -35,7 +37,7 @@ def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
|
||||
def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
@@ -48,7 +50,9 @@ def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
|
||||
|
||||
def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--debug', '--show-rc', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--debug', '--show-rc', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -61,7 +65,7 @@ def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
|
||||
def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -75,7 +79,9 @@ def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
storage_config = {'lock_wait': 5}
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--lock-wait', '5', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--lock-wait', '5', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
@@ -88,7 +94,7 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
||||
storage_config = {}
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING
|
||||
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
@@ -100,7 +106,7 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
||||
|
||||
def test_list_archives_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'list', 'repo'), output_log_level=logging.WARNING
|
||||
('borg1', 'list', 'repo'), output_log_level=logging.WARNING, error_on_warnings=False
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
@@ -113,7 +119,9 @@ def test_list_archives_with_local_path_calls_borg_via_local_path():
|
||||
|
||||
def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--remote-path', 'borg1', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--remote-path', 'borg1', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
)
|
||||
|
||||
module.list_archives(
|
||||
@@ -126,7 +134,9 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
|
||||
|
||||
def test_list_archives_with_short_calls_borg_with_short_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--short', 'repo'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--short', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
@@ -154,6 +164,7 @@ def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
error_on_warnings=False,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
@@ -169,6 +180,7 @@ 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,
|
||||
error_on_warnings=False,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
@@ -180,7 +192,7 @@ def test_list_archives_with_successful_calls_borg_to_exclude_checkpoints():
|
||||
|
||||
def test_list_archives_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None, error_on_warnings=False
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.list_archives(
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import mount as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command, error_on_warnings=False
|
||||
).once()
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_required_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt'))
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_path_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', 'repo::archive', '/mnt', 'path1', 'path2'))
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=['path1', 'path2'],
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_remote_path_parameters():
|
||||
insert_execute_command_mock(
|
||||
('borg', 'mount', '--remote-path', 'borg1', 'repo::archive', '/mnt')
|
||||
)
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={},
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_umask_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', '--umask', '0770', 'repo::archive', '/mnt'))
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={'umask': '0770'},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_lock_wait_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', '--lock-wait', '5', 'repo::archive', '/mnt'))
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={'lock_wait': '5'},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
insert_execute_command_mock(('borg', 'mount', '--info', 'repo::archive', '/mnt'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', '--debug', '--show-rc', 'repo::archive', '/mnt'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options=None,
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_foreground_parameter():
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'mount', '--foreground', 'repo::archive', '/mnt'), error_on_warnings=False
|
||||
).once()
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=True,
|
||||
options=None,
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_mount_archive_calls_borg_with_options_parameters():
|
||||
insert_execute_command_mock(('borg', 'mount', '-o', 'super_mount', 'repo::archive', '/mnt'))
|
||||
|
||||
module.mount_archive(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
mount_point='/mnt',
|
||||
paths=None,
|
||||
foreground=False,
|
||||
options='super_mount',
|
||||
storage_config={},
|
||||
)
|
||||
@@ -10,7 +10,7 @@ from ..test_verbosity import insert_logging_mock
|
||||
|
||||
def insert_execute_command_mock(prune_command, output_log_level):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
prune_command, output_log_level=output_log_level
|
||||
prune_command, output_log_level=output_log_level, error_on_warnings=False
|
||||
).once()
|
||||
|
||||
|
||||
@@ -188,3 +188,18 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
storage_config=storage_config,
|
||||
retention_config=retention_config,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
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 + ('--extra', '--options', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={'extra_borg_options': {'prune': '--extra --options'}},
|
||||
retention_config=retention_config,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import umount as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command, error_on_warnings=True
|
||||
).once()
|
||||
|
||||
|
||||
def test_unmount_archive_calls_borg_with_required_parameters():
|
||||
insert_execute_command_mock(('borg', 'umount', '/mnt'))
|
||||
|
||||
module.unmount_archive(mount_point='/mnt')
|
||||
|
||||
|
||||
def test_unmount_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
insert_execute_command_mock(('borg', 'umount', '--info', '/mnt'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.unmount_archive(mount_point='/mnt')
|
||||
|
||||
|
||||
def test_unmount_archive_with_log_debug_calls_borg_with_debug_parameters():
|
||||
insert_execute_command_mock(('borg', 'umount', '--debug', '--show-rc', '/mnt'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.unmount_archive(mount_point='/mnt')
|
||||
@@ -23,10 +23,7 @@ def test_run_configuration_runs_actions_for_each_repository():
|
||||
def test_run_configuration_executes_hooks_for_create_action():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').twice()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').twice()
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks').twice()
|
||||
flexmock(module.cronitor).should_receive('ping_cronitor').twice()
|
||||
flexmock(module.cronhub).should_receive('ping_cronhub').twice()
|
||||
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
@@ -38,9 +35,6 @@ def test_run_configuration_logs_actions_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks')
|
||||
flexmock(module.cronitor).should_receive('ping_cronitor')
|
||||
flexmock(module.cronhub).should_receive('ping_cronhub')
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
@@ -72,9 +66,6 @@ def test_run_configuration_logs_post_hook_error():
|
||||
OSError
|
||||
).and_return(None)
|
||||
flexmock(module.dispatch).should_receive('call_hooks')
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks')
|
||||
flexmock(module.cronitor).should_receive('ping_cronitor')
|
||||
flexmock(module.cronhub).should_receive('ping_cronhub')
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
@@ -124,36 +115,55 @@ 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_log_record_with_suppress_does_not_raise():
|
||||
module.log_record(levelno=1, foo='bar', baz='quux', suppress_log=True)
|
||||
|
||||
|
||||
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.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||
|
||||
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 == ()
|
||||
@@ -209,6 +219,33 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_mount():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'mount': flexmock(repository='repo')}
|
||||
|
||||
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_mount_with_repository_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||
ValueError
|
||||
)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'mount': flexmock(repository='repo')}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||
arguments = {'global': flexmock(config_paths=[])}
|
||||
expected_logs = (flexmock(),)
|
||||
@@ -277,6 +314,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(
|
||||
|
||||
@@ -3,30 +3,36 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks import cronhub as module
|
||||
|
||||
|
||||
def test_ping_cronhub_hits_ping_url_with_start_state():
|
||||
def test_ping_monitor_rewrites_ping_url_for_start_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
state = 'bork'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/bork/abcdef')
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/start/abcdef')
|
||||
|
||||
module.ping_cronhub(ping_url, 'config.yaml', dry_run=False, state=state)
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_cronhub_hits_ping_url_with_ping_state():
|
||||
def test_ping_monitor_rewrites_ping_url_and_state_for_start_state():
|
||||
ping_url = 'https://example.com/ping/abcdef'
|
||||
state = 'bork'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/bork/abcdef')
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/start/abcdef')
|
||||
|
||||
module.ping_cronhub(ping_url, 'config.yaml', dry_run=False, state=state)
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_cronhub_without_ping_url_does_not_raise():
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
def test_ping_monitor_rewrites_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/finish/abcdef')
|
||||
|
||||
module.ping_cronhub(ping_url=None, config_filename='config.yaml', dry_run=False, state='oops')
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FINISH, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_cronhub_dry_run_does_not_hit_ping_url():
|
||||
def test_ping_monitor_rewrites_ping_url_for_fail_state():
|
||||
ping_url = 'https://example.com/start/abcdef'
|
||||
flexmock(module.requests).should_receive('get').with_args('https://example.com/fail/abcdef')
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FAIL, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_cronhub(ping_url, 'config.yaml', dry_run=True, state='yay')
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=True)
|
||||
|
||||
@@ -3,22 +3,29 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks import cronitor as module
|
||||
|
||||
|
||||
def test_ping_cronitor_hits_ping_url():
|
||||
def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
ping_url = 'https://example.com'
|
||||
append = 'failed-so-hard'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'run'))
|
||||
|
||||
module.ping_cronitor(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_cronitor_without_ping_url_does_not_raise():
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'complete'))
|
||||
|
||||
module.ping_cronitor(ping_url=None, config_filename='config.yaml', dry_run=False, append='oops')
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FINISH, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_cronitor_dry_run_does_not_hit_ping_url():
|
||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, 'fail'))
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.FAIL, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_cronitor(ping_url, 'config.yaml', dry_run=True, append='yay')
|
||||
module.ping_monitor(ping_url, 'config.yaml', module.monitor.State.START, dry_run=True)
|
||||
|
||||
@@ -26,18 +26,45 @@ def test_make_database_dump_filename_with_invalid_name_raises():
|
||||
module.make_database_dump_filename('databases', 'invalid/name')
|
||||
|
||||
|
||||
def test_flatten_dump_patterns_produces_list_of_all_patterns():
|
||||
dump_patterns = {'postgresql_databases': ['*/glob', 'glob/*'], 'mysql_databases': ['*/*/*']}
|
||||
expected_patterns = sorted(
|
||||
dump_patterns['postgresql_databases'] + dump_patterns['mysql_databases']
|
||||
)
|
||||
|
||||
assert sorted(module.flatten_dump_patterns(dump_patterns, ('bob',))) == expected_patterns
|
||||
|
||||
|
||||
def test_flatten_dump_patterns_with_no_patterns_errors():
|
||||
dump_patterns = {'postgresql_databases': [], 'mysql_databases': []}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.flatten_dump_patterns(dump_patterns, ('bob',))
|
||||
|
||||
|
||||
def test_flatten_dump_patterns_with_no_hooks_errors():
|
||||
dump_patterns = {}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.flatten_dump_patterns(dump_patterns, ('bob',))
|
||||
|
||||
|
||||
def test_remove_database_dumps_removes_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_database_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
flexmock(module).should_receive('make_database_dump_filename').with_args(
|
||||
'databases', 'foo', None
|
||||
).and_return('databases/localhost/foo')
|
||||
flexmock(module).should_receive('make_database_dump_filename').with_args(
|
||||
'databases', 'bar', None
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.os).should_receive('listdir').and_return([])
|
||||
flexmock(module.os).should_receive('rmdir')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'databases/localhost/{}'.format(name)
|
||||
).once()
|
||||
flexmock(module.os).should_receive('remove').with_args('databases/localhost/foo').once()
|
||||
flexmock(module.os).should_receive('remove').with_args('databases/localhost/bar').once()
|
||||
flexmock(module.os).should_receive('listdir').with_args('databases/localhost').and_return(
|
||||
['bar']
|
||||
).and_return([])
|
||||
|
||||
flexmock(module.os).should_receive('rmdir').with_args('databases/localhost').once()
|
||||
|
||||
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=False)
|
||||
|
||||
@@ -134,3 +161,15 @@ def test_get_per_hook_database_configurations_with_unknown_database_name_raises(
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_per_hook_database_configurations(hooks, names, dump_patterns)
|
||||
|
||||
|
||||
def test_get_per_hook_database_configurations_with_all_and_no_archive_dumps_raises():
|
||||
hooks = {'postgresql_databases': [flexmock()]}
|
||||
names = ('foo', 'all')
|
||||
dump_patterns = flexmock()
|
||||
flexmock(module).should_receive('get_database_configurations').with_args(
|
||||
hooks['postgresql_databases'], names
|
||||
).and_return([])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.get_per_hook_database_configurations(hooks, names, dump_patterns)
|
||||
|
||||
@@ -3,38 +3,105 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks import healthchecks as module
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args(ping_url)
|
||||
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'))
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False)
|
||||
assert handler.buffer == ['foo\n', 'bar\n']
|
||||
assert not handler.forgot
|
||||
|
||||
|
||||
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_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_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)
|
||||
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()])
|
||||
)
|
||||
|
||||
module.ping_healthchecks(ping_uuid, 'config.yaml', dry_run=False)
|
||||
payload = module.format_buffered_logs_for_payload()
|
||||
|
||||
assert payload == ''
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url_with_append():
|
||||
def test_ping_monitor_hits_ping_url_for_start_state():
|
||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
||||
ping_url = 'https://example.com'
|
||||
append = 'failed-so-hard'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
'{}/{}'.format(ping_url, 'start'), data=''.encode('utf-8')
|
||||
)
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_dry_run_does_not_hit_ping_url():
|
||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
payload = 'data'
|
||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
||||
flexmock(module.requests).should_receive('post').with_args(
|
||||
ping_url, data=payload.encode('utf-8')
|
||||
)
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=True)
|
||||
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'
|
||||
payload = 'data'
|
||||
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.encode('utf')
|
||||
)
|
||||
|
||||
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'
|
||||
payload = 'data'
|
||||
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.encode('utf-8')
|
||||
)
|
||||
|
||||
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('post').never()
|
||||
|
||||
module.ping_monitor(ping_url, 'config.yaml', state=module.monitor.State.START, dry_run=True)
|
||||
|
||||
@@ -4,44 +4,28 @@ from flexmock import flexmock
|
||||
from borgmatic import execute as module
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_error_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 2)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_warning_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 1)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_success_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 0)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_error_and_error_on_warnings_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 2, error_on_warnings=True)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_warning_and_error_on_warnings_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/borg1', 'init'), 1, error_on_warnings=True)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_borg_success_and_error_on_warnings_is_false():
|
||||
assert not module.exit_code_indicates_error(
|
||||
('/usr/bin/borg1', 'init'), 0, error_on_warnings=True
|
||||
@pytest.mark.parametrize(
|
||||
'exit_code,error_on_warnings,expected_result',
|
||||
(
|
||||
(2, True, True),
|
||||
(2, False, True),
|
||||
(1, True, True),
|
||||
(1, False, False),
|
||||
(0, True, False),
|
||||
(0, False, False),
|
||||
),
|
||||
)
|
||||
def test_exit_code_indicates_error_respects_exit_code_and_error_on_warnings(
|
||||
exit_code, error_on_warnings, expected_result
|
||||
):
|
||||
assert (
|
||||
module.exit_code_indicates_error(
|
||||
('command',), exit_code, error_on_warnings=error_on_warnings
|
||||
)
|
||||
is expected_result
|
||||
)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_error_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/command',), 2)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_warning_is_true():
|
||||
assert module.exit_code_indicates_error(('/usr/bin/command',), 1)
|
||||
|
||||
|
||||
def test_exit_code_indicates_error_with_non_borg_success_is_false():
|
||||
assert not module.exit_code_indicates_error(('/usr/bin/command',), 0)
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
|
||||
@@ -203,10 +203,10 @@ def test_configure_logging_to_logfile_instead_of_syslog():
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
file_handler = logging.FileHandler('/tmp/logfile')
|
||||
flexmock(module.logging).should_receive('FileHandler').with_args('/tmp/logfile').and_return(
|
||||
file_handler
|
||||
).once()
|
||||
file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile')
|
||||
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args(
|
||||
'/tmp/logfile'
|
||||
).and_return(file_handler).once()
|
||||
|
||||
module.configure_logging(
|
||||
console_log_level=logging.INFO, log_file_log_level=logging.DEBUG, log_file='/tmp/logfile'
|
||||
@@ -214,12 +214,12 @@ def test_configure_logging_to_logfile_instead_of_syslog():
|
||||
|
||||
|
||||
def test_configure_logging_skips_logfile_if_argument_is_none():
|
||||
# No FileHandler added if argument --log-file is None
|
||||
# No WatchedFileHandler added if argument --log-file is None
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.logging).should_receive('FileHandler').never()
|
||||
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO, log_file=None)
|
||||
|
||||