Compare commits

..

No commits in common. "master" and "1.5.5" have entirely different histories.

106 changed files with 1719 additions and 5535 deletions

View File

@ -1,29 +1,110 @@
---
kind: pipeline
name: python-3-8-alpine-3-13
name: python-3-5-alpine-3-10
services:
- name: postgresql
image: postgres:13.1-alpine
image: postgres:11.6-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: mysql
image: mariadb:10.5
image: mariadb:10.3
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
- name: mongodb
image: mongo:5.0.5
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: test
clone:
skip_verify: true
steps:
- name: build
image: alpine:3.13
image: python:3.5-alpine3.10
pull: always
commands:
- scripts/run-full-tests
---
kind: pipeline
name: python-3-6-alpine-3-10
services:
- name: postgresql
image: postgres:11.6-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: mysql
image: mariadb:10.3
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
steps:
- name: build
image: python:3.6-alpine3.10
pull: always
commands:
- scripts/run-full-tests
---
kind: pipeline
name: python-3-7-alpine-3-10
services:
- name: postgresql
image: postgres:11.6-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: mysql
image: mariadb:10.3
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
steps:
- name: build
image: python:3.7-alpine3.10
pull: always
commands:
- scripts/run-full-tests
---
kind: pipeline
name: python-3-7-alpine-3-7
services:
- name: postgresql
image: postgres:10.11-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: mysql
image: mariadb:10.1
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
steps:
- name: build
image: python:3.7-alpine3.7
pull: always
commands:
- scripts/run-full-tests
---
kind: pipeline
name: python-3-8-alpine-3-10
services:
- name: postgresql
image: postgres:11.6-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: mysql
image: mariadb:10.3
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
steps:
- name: build
image: python:3.8-alpine3.10
pull: always
commands:
- scripts/run-full-tests
@ -31,9 +112,6 @@ steps:
kind: pipeline
name: documentation
clone:
skip_verify: true
steps:
- name: build
image: plugins/docker
@ -44,11 +122,6 @@ steps:
from_secret: docker_password
repo: witten/borgmatic-docs
dockerfile: docs/Dockerfile
trigger:
repo:
- borgmatic-collective/borgmatic
branch:
- master
event:
- push
when:
branch:
- master

View File

@ -1,11 +1,9 @@
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
const navigationPlugin = require("@11ty/eleventy-navigation");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginSyntaxHighlight);
eleventyConfig.addPlugin(inclusiveLangPlugin);
eleventyConfig.addPlugin(navigationPlugin);
let markdownIt = require("markdown-it");
let markdownItAnchor = require("markdown-it-anchor");
@ -36,8 +34,6 @@ module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy({"docs/static": "static"});
eleventyConfig.setLiquidOptions({dynamicPartials: false});
return {
templateFormats: [
"md",

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
*.pyc
*.swp
.cache
.coverage*
.coverage
.pytest_cache
.tox
__pycache__

161
NEWS
View File

@ -1,162 +1,3 @@
1.5.25.dev0
* #516: Fix handling of TERM signal to exit borgmatic, not just forward the signal to Borg.
* #517: Fix borgmatic exit code (so it's zero) when initial Borg calls fail but later retries
succeed.
* Change Healthchecks logs truncation size from 10k bytes to 100k bytes, corresponding to that
same change on Healthchecks.io.
1.5.24
* #431: Add "working_directory" option to support source directories with relative paths.
* #444: When loading a configuration file that is unreadable due to file permissions, warn instead
of erroring. This supports running borgmatic as a non-root user with configuration in ~/.config
even if there is an unreadable global configuration file in /etc.
* #469: Add "repositories" context to "before_*" and "after_*" command action hooks. See the
documentation for more information:
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
* #486: Fix handling of "patterns_from" and "exclude_from" options to error instead of warning when
referencing unreadable files and "create" action is run.
* #507: Fix Borg usage error in the "compact" action when running "borgmatic --dry-run". Now, skip
"compact" entirely during a dry run.
1.5.23
* #394: Compact repository segments and free space with new "borgmatic compact" action. Borg 1.2+
only. Also run "compact" by default when no actions are specified, as "prune" in Borg 1.2 no
longer frees up space unless "compact" is run.
* #394: When using the "atime", "bsd_flags", "numeric_owner", or "remote_rate_limit" options,
tailor the flags passed to Borg depending on the Borg version.
* #480, #482: Fix traceback when a YAML validation error occurs.
1.5.22
* #288: Add database dump hook for MongoDB.
* #470: Move mysqldump options to the beginning of the command due to MySQL bug 30994.
* #471: When command-line configuration override produces a parse error, error cleanly instead of
tracebacking.
* #476: Fix unicode error when restoring particular MySQL databases.
* Drop support for Python 3.6, which has been end-of-lifed.
* Add support for Python 3.10.
1.5.21
* #28: Optionally retry failing backups via "retries" and "retry_wait" configuration options.
* #306: Add "list_options" MySQL configuration option for passing additional arguments to MySQL
list command.
* #459: Add support for old version (2.x) of jsonschema library.
1.5.20
* Re-release with correct version without dev0 tag.
1.5.19
* #387: Fix error when configured source directories are not present on the filesystem at the time
of backup. Now, Borg will complain, but the backup will still continue.
* #455: Mention changing borgmatic path in cron documentation.
* Update sample systemd service file with more granular read-only filesystem settings.
* Move Gitea and GitHub hosting from a personal namespace to an organization for better
collaboration with related projects.
* 1k ★s on GitHub!
1.5.18
* #389: Fix "message too long" error when logging to rsyslog.
* #440: Fix traceback that can occur when dumping a database.
1.5.17
* #437: Fix error when configuration file contains "umask" option.
* Remove test dependency on vim and /dev/urandom.
1.5.16
* #379: Suppress console output in sample crontab and systemd service files.
* #407: Fix syslog logging on FreeBSD.
* #430: Fix hang when restoring a PostgreSQL "tar" format database dump.
* Better error messages! Switch the library used for validating configuration files (from pykwalify
to jsonschema).
* Link borgmatic Ansible role from installation documentation:
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install
1.5.15
* #419: Document use case of running backups conditionally based on laptop power level:
https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/
* #425: Run arbitrary Borg commands with new "borgmatic borg" action. See the documentation for
more information: https://torsion.org/borgmatic/docs/how-to/run-arbitrary-borg-commands/
1.5.14
* #390: Add link to Hetzner storage offering from the documentation.
* #398: Clarify canonical home of borgmatic in documentation.
* #406: Clarify that spaces in path names should not be backslashed in path names.
* #423: Fix error handling to error loudly when Borg gets killed due to running out of memory!
* Fix build so as not to attempt to build and push documentation for a non-master branch.
* "Fix" build failure with Alpine Edge by switching from Edge to Alpine 3.13.
* Move #borgmatic IRC channel from Freenode to Libera Chat due to Freenode takeover drama.
IRC connection info: https://torsion.org/borgmatic/#issues
1.5.13
* #373: Document that passphrase is used for Borg keyfile encryption, not just repokey encryption.
* #404: Add support for ruamel.yaml 0.17.x YAML parsing library.
* Update systemd service example to return a permission error when a system call isn't permitted
(instead of terminating borgmatic outright).
* Drop support for Python 3.5, which has been end-of-lifed.
* Add support for Python 3.9.
* Update versions of test dependencies (test_requirements.txt and test containers).
* Only support black code formatter on Python 3.8+. New black dependencies make installation
difficult on older versions of Python.
* Replace "improve this documentation" form with link to support and ticket tracker.
1.5.12
* Fix for previous release with incorrect version suffix in setup.py. No other changes.
1.5.11
* #341: Add "temporary_directory" option for changing Borg's temporary directory.
* #352: Lock down systemd security settings in sample systemd service file.
* #355: Fix traceback when a database hook value is null in a configuration file.
* #361: Merge override values when specifying the "--override" flag multiple times. The previous
behavior was to take the value of the last "--override" flag only.
* #367: Fix traceback when upgrading old INI-style configuration with upgrade-borgmatic-config.
* #368: Fix signal forwarding from borgmatic to Borg resulting in recursion traceback.
* #369: Document support for Borg placeholders in repository names.
1.5.10
* #347: Add hooks that run for the "extract" action: "before_extract" and "after_extract".
* #350: Fix traceback when a configuration directory is non-readable due to directory permissions.
* Add documentation navigation links on left side of all documentation pages.
* Clarify documentation on configuration overrides, specifically the portion about list syntax:
http://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-overrides
* Clarify documentation overview of monitoring options:
http://torsion.org/borgmatic/docs/how-to/monitor-your-backups/
1.5.9
* #300: Add "borgmatic export-tar" action to export an archive to a tar-formatted file or stream.
* #339: Fix for intermittent timing-related test failure of logging function.
* Clarify database documentation about excluding named pipes and character/block devices to prevent
hangs.
* Add documentation on how to make backups redundant with multiple repositories:
https://torsion.org/borgmatic/docs/how-to/make-backups-redundant/
1.5.8
* #336: Fix for traceback when running Cronitor, Cronhub, and PagerDuty monitor hooks.
1.5.7
* #327: Fix broken pass-through of BORG_* environment variables to Borg.
* #328: Fix duplicate logging to Healthchecks and send "after_*" hooks output to Healthchecks.
* #331: Add SSL support to PostgreSQL database configuration.
* #333: Fix for potential data loss (data not getting backed up) when borgmatic omitted configured
source directories in certain situations. Specifically, this occurred when two source directories
on different filesystems were related by parentage (e.g. "/foo" and "/foo/bar/baz") and the
one_file_system option was enabled.
* Update documentation code fragments theme to better match the rest of the page.
* Improve configuration reference documentation readability via more aggressive word-wrapping in
configuration schema descriptions.
1.5.6
* #292: Allow before_backup and similiar hooks to exit with a soft failure without altering the
monitoring status on Healthchecks or other providers. Support this by waiting to ping monitoring
services with a "start" status until after before_* hooks finish. Failures in before_* hooks
still trigger a monitoring "fail" status.
* #316: Fix hang when a stale database dump named pipe from an aborted borgmatic run remains on
disk.
* #323: Fix for certain configuration options like ssh_command impacting Borg invocations for
separate configuration files.
* #324: Add "borgmatic extract --strip-components" flag to remove leading path components when
extracting an archive.
* Tweak comment indentation in generated configuration file for clarity.
* Link to Borgmacator GNOME AppIndicator from monitoring documentation.
1.5.5
* #314: Fix regression in support for PostgreSQL's "directory" dump format. Unlike other dump
formats, the "directory" dump format does not stream directly to/from Borg.
@ -623,7 +464,7 @@
* #49: Support for Borg experimental --patterns-from and --patterns options for specifying mixed
includes/excludes.
* Moved issue tracker from Taiga to integrated Gitea tracker at
https://projects.torsion.org/borgmatic-collective/borgmatic/issues
https://projects.torsion.org/witten/borgmatic/issues
1.1.12
* #46: Declare dependency on pykwalify 1.6 or above, as older versions yield "Unknown key: version"

View File

@ -11,8 +11,6 @@ borgmatic is simple, configuration-driven backup software for servers and
workstations. Protect your files with client-side encryption. Backup your
databases too. Monitor it all with integrated third-party services.
The canonical home of borgmatic is at <a href="https://torsion.org/borgmatic">https://torsion.org/borgmatic</a>.
Here's an example configuration file:
```yaml
@ -54,9 +52,9 @@ hooks:
```
Want to see borgmatic in action? Check out the <a
href="https://asciinema.org/a/203761?autoplay=1" target="_blank">screencast</a>.
href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
<a href="https://asciinema.org/a/203761?autoplay=1" target="_blank"><img src="https://asciinema.org/a/203761.png" width="480"></a>
<script src="https://asciinema.org/a/203761.js" id="asciicast-203761" async></script>
borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
@ -65,55 +63,61 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px;"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
## Getting started
## How-to guides
Your first step is to [install and configure
borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/).
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/) ⬅ *Start here!*
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Backup to a removable drive or an intermittent server](https://torsion.org/borgmatic/docs/how-to/backup-to-a-removable-drive-or-an-intermittent-server/)
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
For additional documentation, check out the links above for <a
href="https://torsion.org/borgmatic/#documentation">borgmatic how-to and
reference guides</a>.
## Reference guides
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
## Hosting providers
Need somewhere to store your encrypted off-site backups? The following hosting
providers include specific support for Borg/borgmatic—and fund borgmatic
development and hosting when you use these links to sign up. (These are
referral links, but without any tracking scripts or cookies.)
Need somewhere to store your encrypted offsite backups? The following hosting
providers include specific support for Borg/borgmatic. Using these links and
services helps support borgmatic development and hosting. (These are referral
links, but without any tracking scripts or cookies.)
<ul>
<li class="referral"><a href="https://www.rsync.net/cgi-bin/borg.cgi?campaign=borg&adgroup=borgmatic">rsync.net</a>: Cloud Storage provider with full support for borg and any other SSH/SFTP tool</li>
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
</ul>
Additionally, [rsync.net](https://www.rsync.net/products/borg.html) and
[Hetzner](https://www.hetzner.com/storage/storage-box) have compatible storage
offerings, but do not currently fund borgmatic development or hosting.
## Support and contributing
### Issues
You've got issues? Or an idea for a feature enhancement? We've got an [issue
tracker](https://projects.torsion.org/borgmatic-collective/borgmatic/issues). In order to
tracker](https://projects.torsion.org/witten/borgmatic/issues). In order to
create a new issue or comment on an issue, you'll need to [login
first](https://projects.torsion.org/user/login). Note that you can login with
an existing GitHub account if you prefer.
If you'd like to chat with borgmatic developers or users, head on over to the
`#borgmatic` IRC channel on Libera Chat, either via <a
href="https://web.libera.chat/#borgmatic">web chat</a> or a
native <a href="ircs://irc.libera.chat:6697">IRC client</a>. If you
don't get a response right away, please hang around a while—or file a ticket
instead.
`#borgmatic` IRC channel on Freenode, either via <a
href="https://webchat.freenode.net/?channels=borgmatic">web chat</a> or a
native <a href="irc://chat.freenode.net:6697">IRC client</a>.
Also see the [security
policy](https://torsion.org/borgmatic/docs/security-policy/) for any security
@ -125,16 +129,16 @@ Other questions or comments? Contact
### Contributing
borgmatic [source code is
available](https://projects.torsion.org/borgmatic-collective/borgmatic) and is also mirrored
on [GitHub](https://github.com/borgmatic-collective/borgmatic) for convenience.
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
available](https://projects.torsion.org/witten/borgmatic), and is also
mirrored on [GitHub](https://github.com/witten/borgmatic) for convenience.
borgmatic is licensed under the GNU General Public License version 3 or any
later version.
If you'd like to contribute to borgmatic development, please feel free to
submit a [Pull Request](https://projects.torsion.org/borgmatic-collective/borgmatic/pulls)
or open an [issue](https://projects.torsion.org/borgmatic-collective/borgmatic/issues) first
submit a [Pull Request](https://projects.torsion.org/witten/borgmatic/pulls)
or open an [issue](https://projects.torsion.org/witten/borgmatic/issues) first
to discuss your idea. We also accept Pull Requests on GitHub, if that's more
your thing. In general, contributions are very welcome. We don't bite!
@ -142,5 +146,5 @@ 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/borgmatic-collective/borgmatic" alt="build status">![Build Status](https://build.torsion.org/api/badges/borgmatic-collective/borgmatic/status.svg?ref=refs/heads/master)</a>
<a href="https://build.torsion.org/witten/borgmatic" alt="build status">![Build Status](https://build.torsion.org/api/badges/witten/borgmatic/status.svg?ref=refs/heads/master)</a>

View File

@ -6,13 +6,14 @@ permalink: security-policy/index.html
## Supported versions
While we want to hear about security vulnerabilities in all versions of
borgmatic, security fixes are only made to the most recently released version.
It's simply not practical for our small volunteer effort to maintain multiple
release branches and put out separate security patches for each.
borgmatic, security fixes will only be made to the most recently released
version. It's not practical for our small volunteer effort to maintain
multiple different release branches and put out separate security patches for
each.
## Reporting a vulnerability
If you find a security vulnerability, please [file a
ticket](https://torsion.org/borgmatic/#issues) or [send email
directly](mailto:witten@torsion.org) as appropriate. You should expect to hear
back within a few days at most and generally sooner.
back within a few days at most, and generally sooner.

View File

@ -1,45 +0,0 @@
import logging
from borgmatic.borg.flags import make_flags
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
REPOSITORYLESS_BORG_COMMANDS = {'serve', None}
def run_arbitrary_borg(
repository, storage_config, options, archive=None, local_path='borg', remote_path=None
):
'''
Given a local or remote repository path, a storage config dict, a sequence of arbitrary
command-line Borg options, and an optional archive name, run an arbitrary Borg command on the
given repository/archive.
'''
lock_wait = storage_config.get('lock_wait', None)
try:
options = options[1:] if options[0] == '--' else options
borg_command = options[0]
command_options = tuple(options[1:])
except IndexError:
borg_command = None
command_options = ()
repository_archive = '::'.join((repository, archive)) if repository and archive else repository
full_command = (
(local_path,)
+ ((borg_command,) if borg_command else ())
+ ((repository_archive,) if borg_command and repository_archive else ())
+ command_options
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ make_flags('remote-path', remote_path)
+ make_flags('lock-wait', lock_wait)
)
return execute_command(
full_command, output_log_level=logging.WARNING, borg_local_path=local_path,
)

View File

@ -1,41 +0,0 @@
import logging
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def compact_segments(
dry_run,
repository,
storage_config,
local_path='borg',
remote_path=None,
progress=False,
cleanup_commits=False,
threshold=None,
):
'''
Given dry-run flag, a local or remote repository path, and a storage config dict, compact Borg
segments in a repository.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('compact', '')
full_command = (
(local_path, 'compact')
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--progress',) if progress else ())
+ (('--cleanup-commits',) if cleanup_commits else ())
+ (('--threshold', str(threshold)) if threshold else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+ (repository,)
)
if not dry_run:
execute_command(full_command, output_log_level=logging.INFO, borg_local_path=local_path)

View File

@ -5,13 +5,12 @@ import os
import pathlib
import tempfile
from borgmatic.borg import feature
from borgmatic.execute import DO_NOT_CAPTURE, execute_command, execute_command_with_processes
logger = logging.getLogger(__name__)
def expand_directory(directory):
def _expand_directory(directory):
'''
Given a directory path, expand any tilde (representing a user's home directory) and any globs
therein. Return a list of one or more resulting paths.
@ -21,7 +20,7 @@ def expand_directory(directory):
return glob.glob(expanded_directory) or [expanded_directory]
def expand_directories(directories):
def _expand_directories(directories):
'''
Given a sequence of directory paths, expand tildes and globs in each one. Return all the
resulting directories as a single flattened tuple.
@ -30,11 +29,11 @@ def expand_directories(directories):
return ()
return tuple(
itertools.chain.from_iterable(expand_directory(directory) for directory in directories)
itertools.chain.from_iterable(_expand_directory(directory) for directory in directories)
)
def expand_home_directories(directories):
def _expand_home_directories(directories):
'''
Given a sequence of directory paths, expand tildes in each one. Do not perform any globbing.
Return the results as a tuple.
@ -45,29 +44,10 @@ def expand_home_directories(directories):
return tuple(os.path.expanduser(directory) for directory in directories)
def map_directories_to_devices(directories):
def deduplicate_directories(directories):
'''
Given a sequence of directories, return a map from directory to an identifier for the device on
which that directory resides or None if the path doesn't exist.
This is handy for determining whether two different directories are on the same filesystem (have
the same device identifier).
'''
return {
directory: os.stat(directory).st_dev if os.path.exists(directory) else None
for directory in directories
}
def deduplicate_directories(directory_devices):
'''
Given a map from directory to the identifier for the device on which that directory resides,
return the directories as a sorted tuple with all duplicate child directories removed. For
instance, if paths is ('/foo', '/foo/bar'), return just: ('/foo',)
The one exception to this rule is if two paths are on different filesystems (devices). In that
case, they won't get de-duplicated in case they both need to be passed to Borg (e.g. the
location.one_file_system option is true).
Given a sequence of directories, return them as a sorted tuple with all duplicate child
directories removed. For instance, if paths is ('/foo', '/foo/bar'), return just: ('/foo',)
The idea is that if Borg is given a parent directory, then it doesn't also need to be given
child directories, because it will naturally spider the contents of the parent directory. And
@ -76,29 +56,24 @@ def deduplicate_directories(directory_devices):
Borg.
'''
deduplicated = set()
directories = sorted(directory_devices.keys())
for directory in directories:
deduplicated.add(directory)
parents = pathlib.PurePath(directory).parents
for directory in sorted(directories):
# If the directory is "/", that contains all child directories, so we can early out.
if directory == os.path.sep:
return (os.path.sep,)
# If another directory in the given list is a parent of current directory (even n levels
# up) and both are on the same filesystem, then the current directory is a duplicate.
for other_directory in directories:
for parent in parents:
if (
pathlib.PurePath(other_directory) == parent
and directory_devices[directory] is not None
and directory_devices[other_directory] == directory_devices[directory]
):
if directory in deduplicated:
deduplicated.remove(directory)
break
# If no other directories are parents of current directory (even n levels up), then the
# current directory isn't a duplicate.
if not any(
pathlib.PurePath(other_directory) in pathlib.PurePath(directory).parents
for other_directory in directories
):
deduplicated.add(directory)
return tuple(sorted(deduplicated))
def write_pattern_file(patterns=None):
def _write_pattern_file(patterns=None):
'''
Given a sequence of patterns, write them to a named temporary file and return it. Return None
if no patterns are provided.
@ -113,19 +88,7 @@ def write_pattern_file(patterns=None):
return pattern_file
def ensure_files_readable(*filename_lists):
'''
Given a sequence of filename sequences, ensure that each filename is openable. This prevents
unreadable files from being passed to Borg, which in certain situations only warns instead of
erroring.
'''
for file_object in itertools.chain.from_iterable(
filename_list for filename_list in filename_lists if filename_list
):
open(file_object).close()
def make_pattern_flags(location_config, pattern_filename=None):
def _make_pattern_flags(location_config, pattern_filename=None):
'''
Given a location config dict with a potential patterns_from option, and a filename containing
any additional patterns, return the corresponding Borg flags for those files as a tuple.
@ -141,7 +104,7 @@ def make_pattern_flags(location_config, pattern_filename=None):
)
def make_exclude_flags(location_config, exclude_filename=None):
def _make_exclude_flags(location_config, exclude_filename=None):
'''
Given a location config dict with various exclude options, and a filename containing any exclude
patterns, return the corresponding Borg flags as a tuple.
@ -200,7 +163,6 @@ def create_archive(
repository,
location_config,
storage_config,
local_borg_version,
local_path='borg',
remote_path=None,
progress=False,
@ -217,21 +179,15 @@ def create_archive(
create command while also triggering the given processes to produce output.
'''
sources = deduplicate_directories(
map_directories_to_devices(
expand_directories(
location_config['source_directories']
+ borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
)
_expand_directories(
location_config['source_directories']
+ borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
)
)
try:
working_directory = os.path.expanduser(location_config.get('working_directory'))
except TypeError:
working_directory = None
pattern_file = write_pattern_file(location_config.get('patterns'))
exclude_file = write_pattern_file(
expand_home_directories(location_config.get('exclude_patterns'))
pattern_file = _write_pattern_file(location_config.get('patterns'))
exclude_file = _write_pattern_file(
_expand_home_directories(location_config.get('exclude_patterns'))
)
checkpoint_interval = storage_config.get('checkpoint_interval', None)
chunker_params = storage_config.get('chunker_params', None)
@ -243,52 +199,25 @@ def create_archive(
archive_name_format = storage_config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
extra_borg_options = storage_config.get('extra_borg_options', {}).get('create', '')
if feature.available(feature.Feature.ATIME, local_borg_version):
atime_flags = ('--atime',) if location_config.get('atime') is True else ()
else:
atime_flags = ('--noatime',) if location_config.get('atime') is False else ()
if feature.available(feature.Feature.NOFLAGS, local_borg_version):
noflags_flags = ('--noflags',) if location_config.get('bsd_flags') is False else ()
else:
noflags_flags = ('--nobsdflags',) if location_config.get('bsd_flags') is False else ()
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else ()
if feature.available(feature.Feature.UPLOAD_RATELIMIT, local_borg_version):
upload_ratelimit_flags = (
('--upload-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
)
else:
upload_ratelimit_flags = (
('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ()
)
ensure_files_readable(location_config.get('patterns_from'), location_config.get('exclude_from'))
full_command = (
tuple(local_path.split(' '))
+ ('create',)
+ make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
+ make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
(local_path, 'create')
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
+ (('--chunker-params', chunker_params) if chunker_params else ())
+ (('--compression', compression) if compression else ())
+ upload_ratelimit_flags
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
+ (
('--one-file-system',)
if location_config.get('one_file_system') or stream_processes
else ()
)
+ numeric_ids_flags
+ atime_flags
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
+ (('--noatime',) if location_config.get('atime') is False else ())
+ (('--noctime',) if location_config.get('ctime') is False else ())
+ (('--nobirthtime',) if location_config.get('birthtime') is False else ())
+ (('--read-special',) if (location_config.get('read_special') or stream_processes) else ())
+ noflags_flags
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
+ (('--files-cache', files_cache) if files_cache else ())
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
@ -327,13 +256,6 @@ def create_archive(
output_log_level,
output_file,
borg_local_path=local_path,
working_directory=working_directory,
)
return execute_command(
full_command,
output_log_level,
output_file,
borg_local_path=local_path,
working_directory=working_directory,
)
return execute_command(full_command, output_log_level, output_file, borg_local_path=local_path)

View File

@ -9,7 +9,6 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
'encryption_passcommand': 'BORG_PASSCOMMAND',
'encryption_passphrase': 'BORG_PASSPHRASE',
'ssh_command': 'BORG_RSH',
'temporary_directory': 'TMPDIR',
}
DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
@ -20,15 +19,9 @@ DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
def initialize(storage_config):
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
# Options from borgmatic configuration take precedence over already set BORG_* environment
# variables.
value = storage_config.get(option_name) or os.environ.get(environment_variable_name)
value = storage_config.get(option_name)
if value:
os.environ[environment_variable_name] = value
else:
os.environ.pop(environment_variable_name, None)
for (
option_name,

View File

@ -1,64 +0,0 @@
import logging
import os
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
def export_tar_archive(
dry_run,
repository,
archive,
paths,
destination_path,
storage_config,
local_path='borg',
remote_path=None,
tar_filter=None,
files=False,
strip_components=None,
):
'''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
export from the archive, a destination path to export to, a storage configuration dict, optional
local and remote Borg paths, an optional filter program, whether to include per-file details,
and an optional number of path components to strip, export the archive into the given
destination path as a tar-formatted file.
If the destination path is "-", then stream the output to stdout instead of to a file.
'''
umask = storage_config.get('umask', None)
lock_wait = storage_config.get('lock_wait', None)
full_command = (
(local_path, 'export-tar')
+ (('--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 ())
+ (('--list',) if files else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--tar-filter', tar_filter) if tar_filter else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),)
+ (destination_path,)
+ (tuple(paths) if paths else ())
)
if files and logger.getEffectiveLevel() == logging.WARNING:
output_log_level = logging.WARNING
else:
output_log_level = logging.INFO
if dry_run:
logging.info('{}: Skipping export to tar file (dry run)'.format(repository))
return
execute_command(
full_command,
output_file=DO_NOT_CAPTURE if destination_path == '-' else None,
output_log_level=output_log_level,
borg_local_path=local_path,
)

View File

@ -2,7 +2,6 @@ import logging
import os
import subprocess
from borgmatic.borg import feature
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
logger = logging.getLogger(__name__)
@ -62,19 +61,17 @@ def extract_archive(
paths,
location_config,
storage_config,
local_borg_version,
local_path='borg',
remote_path=None,
destination_path=None,
strip_components=None,
progress=False,
extract_to_stdout=False,
):
'''
Given a dry-run flag, a local or remote repository path, an archive name, zero or more paths to
restore from the archive, the local Borg version string, location/storage configuration dicts,
optional local and remote Borg paths, and an optional destination path to extract to, extract
the archive into the current directory.
restore from the archive, location/storage configuration dicts, optional local and remote Borg
paths, and an optional destination path to extract to, extract the archive into the current
directory.
If extract to stdout is True, then start the extraction streaming to stdout, and return that
extract process as an instance of subprocess.Popen.
@ -85,21 +82,15 @@ def extract_archive(
if progress and extract_to_stdout:
raise ValueError('progress and extract_to_stdout cannot both be set')
if feature.available(feature.Feature.NUMERIC_IDS, local_borg_version):
numeric_ids_flags = ('--numeric-ids',) if location_config.get('numeric_owner') else ()
else:
numeric_ids_flags = ('--numeric-owner',) if location_config.get('numeric_owner') else ()
full_command = (
(local_path, 'extract')
+ (('--remote-path', remote_path) if remote_path else ())
+ numeric_ids_flags
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--dry-run',) if dry_run else ())
+ (('--strip-components', str(strip_components)) if strip_components else ())
+ (('--progress',) if progress else ())
+ (('--stdout',) if extract_to_stdout else ())
+ ('::'.join((repository if ':' in repository else os.path.abspath(repository), archive)),)

View File

@ -1,28 +0,0 @@
from enum import Enum
from pkg_resources import parse_version
class Feature(Enum):
COMPACT = 1
ATIME = 2
NOFLAGS = 3
NUMERIC_IDS = 4
UPLOAD_RATELIMIT = 5
FEATURE_TO_MINIMUM_BORG_VERSION = {
Feature.COMPACT: parse_version('1.2.0a2'), # borg compact
Feature.ATIME: parse_version('1.2.0a7'), # borg create --atime
Feature.NOFLAGS: parse_version('1.2.0a8'), # borg create --noflags
Feature.NUMERIC_IDS: parse_version('1.2.0b3'), # borg create/extract/mount --numeric-ids
Feature.UPLOAD_RATELIMIT: parse_version('1.2.0b3'), # borg create --upload-ratelimit
}
def available(feature, borg_version):
'''
Given a Borg Feature constant and a Borg version string, return whether that feature is
available in that version of Borg.
'''
return FEATURE_TO_MINIMUM_BORG_VERSION[feature] <= parse_version(borg_version)

View File

@ -1,25 +0,0 @@
import logging
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
def local_borg_version(local_path='borg'):
'''
Given a local Borg binary path, return a version string for it.
Raise OSError or CalledProcessError if there is a problem running Borg.
Raise ValueError if the version cannot be parsed.
'''
full_command = (
(local_path, '--version')
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
)
output = execute_command(full_command, output_log_level=None, borg_local_path=local_path)
try:
return output.split(' ')[1].strip()
except IndexError:
raise ValueError('Could not parse Borg version string')

View File

@ -1,33 +1,30 @@
import collections
from argparse import Action, ArgumentParser
from argparse import ArgumentParser
from borgmatic.config import collect
SUBPARSER_ALIASES = {
'init': ['--init', '-I'],
'prune': ['--prune', '-p'],
'compact': [],
'create': ['--create', '-C'],
'check': ['--check', '-k'],
'extract': ['--extract', '-x'],
'export-tar': ['--export-tar'],
'mount': ['--mount', '-m'],
'umount': ['--umount', '-u'],
'restore': ['--restore', '-r'],
'list': ['--list', '-l'],
'info': ['--info', '-i'],
'borg': [],
}
def parse_subparser_arguments(unparsed_arguments, subparsers):
'''
Given a sequence of arguments and a dict from subparser name to argparse.ArgumentParser
instance, give each requested action's subparser a shot at parsing all arguments. This allows
common arguments like "--repository" to be shared across multiple subparsers.
Given a sequence of arguments, and a subparsers object as returned by
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
parsing all arguments. This allows common arguments like "--repository" to be shared across
multiple subparsers.
Return the result as a tuple of (a dict mapping from subparser name to a parsed namespace of
arguments, a list of remaining arguments not claimed by any subparser).
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
'''
arguments = collections.OrderedDict()
remaining_arguments = list(unparsed_arguments)
@ -37,12 +34,7 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
for alias in aliases
}
# If the "borg" action is used, skip all other subparsers. This avoids confusion like
# "borg list" triggering borgmatic's own list action.
if 'borg' in unparsed_arguments:
subparsers = {'borg': subparsers['borg']}
for subparser_name, subparser in subparsers.items():
for subparser_name, subparser in subparsers.choices.items():
if subparser_name not in remaining_arguments:
continue
@ -54,59 +46,59 @@ def parse_subparser_arguments(unparsed_arguments, subparsers):
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
for value in vars(parsed).values():
if isinstance(value, str):
if value in subparsers:
if value in subparsers.choices:
remaining_arguments.remove(value)
elif isinstance(value, list):
for item in value:
if item in subparsers:
if item in subparsers.choices:
remaining_arguments.remove(item)
arguments[canonical_name] = parsed
# If no actions are explicitly requested, assume defaults: prune, compact, create, and check.
# If no actions are explicitly requested, assume defaults: prune, create, and check.
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
for subparser_name in ('prune', 'compact', 'create', 'check'):
subparser = subparsers[subparser_name]
for subparser_name in ('prune', 'create', 'check'):
subparser = subparsers.choices[subparser_name]
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
arguments[subparser_name] = parsed
remaining_arguments = list(unparsed_arguments)
return arguments
# Now ask each subparser, one by one, to greedily consume arguments.
for subparser_name, subparser in subparsers.items():
if subparser_name not in arguments.keys():
def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
'''
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
arguments as a parsed argparse.Namespace instance.
'''
# Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
# are global arguments.
remaining_arguments = list(unparsed_arguments)
present_subparser_names = set()
for subparser_name, subparser in subparsers.choices.items():
if subparser_name not in remaining_arguments:
continue
subparser = subparsers[subparser_name]
present_subparser_names.add(subparser_name)
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# Special case: If "borg" is present in the arguments, consume all arguments after (+1) the
# "borg" action.
if 'borg' in arguments:
borg_options_index = remaining_arguments.index('borg') + 1
arguments['borg'].options = remaining_arguments[borg_options_index:]
remaining_arguments = remaining_arguments[:borg_options_index]
# If no actions are explicitly requested, assume defaults: prune, create, and check.
if (
not present_subparser_names
and '--help' not in unparsed_arguments
and '-h' not in unparsed_arguments
):
for subparser_name in ('prune', 'create', 'check'):
subparser = subparsers.choices[subparser_name]
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# Remove the subparser names themselves.
for subparser_name, subparser in subparsers.items():
for subparser_name in present_subparser_names:
if subparser_name in remaining_arguments:
remaining_arguments.remove(subparser_name)
return (arguments, remaining_arguments)
class Extend_action(Action):
'''
An argparse action to support Python 3.8's "extend" action in older versions of Python.
'''
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest, None)
if items:
items.extend(values)
else:
setattr(namespace, self.dest, list(values))
return top_level_parser.parse_args(remaining_arguments)
def parse_arguments(*unparsed_arguments):
@ -118,7 +110,6 @@ def parse_arguments(*unparsed_arguments):
unexpanded_config_paths = collect.get_default_config_paths(expand_home=False)
global_parser = ArgumentParser(add_help=False)
global_parser.register('action', 'extend', Extend_action)
global_group = global_parser.add_argument_group('global arguments')
global_group.add_argument(
@ -186,7 +177,6 @@ def parse_arguments(*unparsed_arguments):
metavar='SECTION.OPTION=VALUE',
nargs='+',
dest='overrides',
action='extend',
help='One or more configuration file options to override with specified values',
)
global_group.add_argument(
@ -200,8 +190,8 @@ def parse_arguments(*unparsed_arguments):
top_level_parser = ArgumentParser(
description='''
Simple, configuration-driven backup software for servers and workstations. If none of
the action options are given, then borgmatic defaults to: prune, compact, create, and
check.
the action options are given, then borgmatic defaults to: prune, create, and check
archives.
''',
parents=[global_parser],
)
@ -209,7 +199,7 @@ def parse_arguments(*unparsed_arguments):
subparsers = top_level_parser.add_subparsers(
title='actions',
metavar='',
help='Specify zero or more actions. Defaults to prune, compact, create, and check. Use --help with action for details:',
help='Specify zero or more actions. Defaults to prune, create, and check. Use --help with action for details:',
)
init_parser = subparsers.add_parser(
'init',
@ -242,8 +232,8 @@ def parse_arguments(*unparsed_arguments):
prune_parser = subparsers.add_parser(
'prune',
aliases=SUBPARSER_ALIASES['prune'],
help='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
description='Prune archives according to the retention policy (with Borg 1.2+, run compact afterwards to actually free space)',
help='Prune archives according to the retention policy',
description='Prune archives according to the retention policy',
add_help=False,
)
prune_group = prune_parser.add_argument_group('prune arguments')
@ -259,38 +249,6 @@ def parse_arguments(*unparsed_arguments):
)
prune_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
compact_parser = subparsers.add_parser(
'compact',
aliases=SUBPARSER_ALIASES['compact'],
help='Compact segments to free space (Borg 1.2+ only)',
description='Compact segments to free space (Borg 1.2+ only)',
add_help=False,
)
compact_group = compact_parser.add_argument_group('compact arguments')
compact_group.add_argument(
'--progress',
dest='progress',
default=False,
action='store_true',
help='Display progress as each segment is compacted',
)
compact_group.add_argument(
'--cleanup-commits',
dest='cleanup_commits',
default=False,
action='store_true',
help='Cleanup commit-only 17-byte segment files left behind by Borg 1.1',
)
compact_group.add_argument(
'--threshold',
type=int,
dest='threshold',
help='Minimum saved space percentage threshold for compacting a segment, defaults to 10',
)
compact_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
create_parser = subparsers.add_parser(
'create',
aliases=SUBPARSER_ALIASES['create'],
@ -382,13 +340,6 @@ def parse_arguments(*unparsed_arguments):
dest='destination',
help='Directory to extract files into, defaults to the current directory',
)
extract_group.add_argument(
'--strip-components',
type=int,
metavar='NUMBER',
dest='strip_components',
help='Number of leading path components to remove from each extracted path. Skip paths with fewer elements',
)
extract_group.add_argument(
'--progress',
dest='progress',
@ -400,52 +351,6 @@ def parse_arguments(*unparsed_arguments):
'-h', '--help', action='help', help='Show this help message and exit'
)
export_tar_parser = subparsers.add_parser(
'export-tar',
aliases=SUBPARSER_ALIASES['export-tar'],
help='Export an archive to a tar-formatted file or stream',
description='Export an archive to a tar-formatted file or stream',
add_help=False,
)
export_tar_group = export_tar_parser.add_argument_group('export-tar arguments')
export_tar_group.add_argument(
'--repository',
help='Path of repository to export from, defaults to the configured repository if there is only one',
)
export_tar_group.add_argument(
'--archive', help='Name of archive to export (or "latest")', required=True
)
export_tar_group.add_argument(
'--path',
metavar='PATH',
nargs='+',
dest='paths',
help='Paths to export from archive, defaults to the entire archive',
)
export_tar_group.add_argument(
'--destination',
metavar='PATH',
dest='destination',
help='Path to destination export tar file, or "-" for stdout (but be careful about dirtying output with --verbosity or --files)',
required=True,
)
export_tar_group.add_argument(
'--tar-filter', help='Name of filter program to pipe data through'
)
export_tar_group.add_argument(
'--files', default=False, action='store_true', help='Show per-file details'
)
export_tar_group.add_argument(
'--strip-components',
type=int,
metavar='NUMBER',
dest='strip_components',
help='Number of leading path components to remove from each exported path. Skip paths with fewer elements',
)
export_tar_group.add_argument(
'-h', '--help', action='help', help='Show this help message and exit'
)
mount_parser = subparsers.add_parser(
'mount',
aliases=SUBPARSER_ALIASES['mount'],
@ -535,7 +440,8 @@ def parse_arguments(*unparsed_arguments):
)
list_group = list_parser.add_argument_group('list arguments')
list_group.add_argument(
'--repository', help='Path of repository to list, defaults to the configured repositories',
'--repository',
help='Path of repository to list, defaults to the configured repository if there is only one',
)
list_group.add_argument('--archive', help='Name of archive to list (or "latest")')
list_group.add_argument(
@ -621,36 +527,12 @@ def parse_arguments(*unparsed_arguments):
help='Show info for first N archives after other filters are applied',
)
info_group.add_argument(
'--last', metavar='N', help='Show info for last N archives after other filters are applied'
'--last', metavar='N', help='Show info for first N archives after other filters are applied'
)
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
borg_parser = subparsers.add_parser(
'borg',
aliases=SUBPARSER_ALIASES['borg'],
help='Run an arbitrary Borg command',
description='Run an arbitrary Borg command based on borgmatic\'s configuration',
add_help=False,
)
borg_group = borg_parser.add_argument_group('borg arguments')
borg_group.add_argument(
'--repository',
help='Path of repository to pass to Borg, defaults to the configured repositories',
)
borg_group.add_argument('--archive', help='Name of archive to pass to Borg (or "latest")')
borg_group.add_argument(
'--',
metavar='OPTION',
dest='options',
nargs='+',
help='Options to pass to Borg, command first ("create", "list", etc). "--" is optional. To specify the repository or the archive, you must use --repository or --archive instead of providing them here.',
)
borg_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
arguments, remaining_arguments = parse_subparser_arguments(
unparsed_arguments, subparsers.choices
)
arguments['global'] = top_level_parser.parse_args(remaining_arguments)
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
if arguments['global'].excludes_filename:
raise ValueError(

View File

@ -4,28 +4,21 @@ import json
import logging
import os
import sys
import time
from queue import Queue
from subprocess import CalledProcessError
import colorama
import pkg_resources
from borgmatic.borg import borg as borg_borg
from borgmatic.borg import check as borg_check
from borgmatic.borg import compact as borg_compact
from borgmatic.borg import create as borg_create
from borgmatic.borg import environment as borg_environment
from borgmatic.borg import export_tar as borg_export_tar
from borgmatic.borg import extract as borg_extract
from borgmatic.borg import feature as borg_feature
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.borg import version as borg_version
from borgmatic.commands.arguments import parse_arguments
from borgmatic.config import checks, collect, convert, validate
from borgmatic.hooks import command, dispatch, dump, monitor
@ -41,8 +34,8 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
def run_configuration(config_filename, config, arguments):
'''
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
dict from subparser name to a namespace of parsed arguments, execute the defined prune, compact,
create, check, and/or other actions.
dict from subparser name to a namespace of parsed arguments, execute its defined pruning,
backups, consistency checks, and/or other actions.
Yield a combination of:
@ -57,33 +50,20 @@ def run_configuration(config_filename, config, arguments):
local_path = location.get('local_path', 'borg')
remote_path = location.get('remote_path')
retries = storage.get('retries', 0)
retry_wait = storage.get('retry_wait', 0)
borg_environment.initialize(storage)
encountered_error = None
error_repository = ''
using_primary_action = {'prune', 'compact', 'create', 'check'}.intersection(arguments)
prune_create_or_check = {'prune', 'create', 'check'}.intersection(arguments)
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
hook_context = {
'repositories': ','.join(location['repositories']),
}
try:
local_borg_version = borg_version.local_borg_version(local_path)
except (OSError, CalledProcessError, ValueError) as error:
yield from log_error_records(
'{}: Error getting local Borg version'.format(config_filename), error
)
return
try:
if using_primary_action:
if prune_create_or_check:
dispatch.call_hooks(
'initialize_monitor',
'ping_monitor',
hooks,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.START,
monitoring_log_level,
global_arguments.dry_run,
)
@ -94,15 +74,6 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'pre-prune',
global_arguments.dry_run,
**hook_context,
)
if 'compact' in arguments:
command.execute_hook(
hooks.get('before_compact'),
hooks.get('umask'),
config_filename,
'pre-compact',
global_arguments.dry_run,
)
if 'create' in arguments:
command.execute_hook(
@ -111,7 +82,6 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'pre-backup',
global_arguments.dry_run,
**hook_context,
)
if 'check' in arguments:
command.execute_hook(
@ -120,45 +90,18 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'pre-check',
global_arguments.dry_run,
**hook_context,
)
if 'extract' in arguments:
command.execute_hook(
hooks.get('before_extract'),
hooks.get('umask'),
config_filename,
'pre-extract',
global_arguments.dry_run,
**hook_context,
)
if using_primary_action:
dispatch.call_hooks(
'ping_monitor',
hooks,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitor.State.START,
monitoring_log_level,
global_arguments.dry_run,
)
except (OSError, CalledProcessError) as error:
if command.considered_soft_failure(config_filename, error):
return
encountered_error = error
yield from log_error_records('{}: Error running pre hook'.format(config_filename), error)
yield from make_error_log_records(
'{}: Error running pre hook'.format(config_filename), error
)
if not encountered_error:
repo_queue = Queue()
for repo in location['repositories']:
repo_queue.put((repo, 0),)
while not repo_queue.empty():
repository_path, retry_num = repo_queue.get()
timeout = retry_num * retry_wait
if timeout:
logger.warning(f'{config_filename}: Sleeping {timeout}s before next retry')
time.sleep(timeout)
for repository_path in location['repositories']:
try:
yield from run_actions(
arguments=arguments,
@ -169,30 +112,14 @@ def run_configuration(config_filename, config, arguments):
hooks=hooks,
local_path=local_path,
remote_path=remote_path,
local_borg_version=local_borg_version,
repository_path=repository_path,
)
except (OSError, CalledProcessError, ValueError) as error:
if retry_num < retries:
repo_queue.put((repository_path, retry_num + 1),)
tuple( # Consume the generator so as to trigger logging.
log_error_records(
'{}: Error running actions for repository'.format(repository_path),
error,
levelno=logging.WARNING,
log_command_error_output=True,
)
)
logger.warning(
f'{config_filename}: Retrying... attempt {retry_num + 1}/{retries}'
)
continue
yield from log_error_records(
'{}: Error running actions for repository'.format(repository_path), error
)
encountered_error = error
error_repository = repository_path
yield from make_error_log_records(
'{}: Error running actions for repository'.format(repository_path), error
)
if not encountered_error:
try:
@ -203,15 +130,6 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'post-prune',
global_arguments.dry_run,
**hook_context,
)
if 'compact' in arguments:
command.execute_hook(
hooks.get('after_compact'),
hooks.get('umask'),
config_filename,
'post-compact',
global_arguments.dry_run,
)
if 'create' in arguments:
dispatch.call_hooks(
@ -228,7 +146,6 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'post-backup',
global_arguments.dry_run,
**hook_context,
)
if 'check' in arguments:
command.execute_hook(
@ -237,18 +154,8 @@ def run_configuration(config_filename, config, arguments):
config_filename,
'post-check',
global_arguments.dry_run,
**hook_context,
)
if 'extract' in arguments:
command.execute_hook(
hooks.get('after_extract'),
hooks.get('umask'),
config_filename,
'post-extract',
global_arguments.dry_run,
**hook_context,
)
if using_primary_action:
if {'prune', 'create', 'check'}.intersection(arguments):
dispatch.call_hooks(
'ping_monitor',
hooks,
@ -258,24 +165,16 @@ def run_configuration(config_filename, config, arguments):
monitoring_log_level,
global_arguments.dry_run,
)
dispatch.call_hooks(
'destroy_monitor',
hooks,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitoring_log_level,
global_arguments.dry_run,
)
except (OSError, CalledProcessError) as error:
if command.considered_soft_failure(config_filename, error):
return
encountered_error = error
yield from log_error_records(
yield from make_error_log_records(
'{}: Error running post hook'.format(config_filename), error
)
if encountered_error and using_primary_action:
if encountered_error and prune_create_or_check:
try:
command.execute_hook(
hooks.get('on_error'),
@ -296,19 +195,11 @@ def run_configuration(config_filename, config, arguments):
monitoring_log_level,
global_arguments.dry_run,
)
dispatch.call_hooks(
'destroy_monitor',
hooks,
config_filename,
monitor.MONITOR_HOOK_NAMES,
monitoring_log_level,
global_arguments.dry_run,
)
except (OSError, CalledProcessError) as error:
if command.considered_soft_failure(config_filename, error):
return
yield from log_error_records(
yield from make_error_log_records(
'{}: Error running on-error hook'.format(config_filename), error
)
@ -323,13 +214,12 @@ def run_actions(
hooks,
local_path,
remote_path,
local_borg_version,
repository_path,
repository_path
): # pragma: no cover
'''
Given parsed command-line arguments as an argparse.ArgumentParser instance, several different
configuration dicts, local and remote paths to Borg, a local Borg version string, and a
repository name, run all actions from the command-line arguments on the given repository.
configuration dicts, local and remote paths to Borg, and a repository name, run all actions
from the command-line arguments on the given repository.
Yield JSON output strings from executing any actions that produce JSON.
@ -362,23 +252,6 @@ def run_actions(
stats=arguments['prune'].stats,
files=arguments['prune'].files,
)
if 'compact' in arguments:
if borg_feature.available(borg_feature.Feature.COMPACT, local_borg_version):
logger.info('{}: Compacting segments{}'.format(repository, dry_run_label))
borg_compact.compact_segments(
global_arguments.dry_run,
repository,
storage,
local_path=local_path,
remote_path=remote_path,
progress=arguments['compact'].progress,
cleanup_commits=arguments['compact'].cleanup_commits,
threshold=arguments['compact'].threshold,
)
else:
logger.info(
'{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository)
)
if 'create' in arguments:
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
dispatch.call_hooks(
@ -404,7 +277,6 @@ def run_actions(
repository,
location,
storage,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
progress=arguments['create'].progress,
@ -444,37 +316,11 @@ def run_actions(
arguments['extract'].paths,
location,
storage,
local_borg_version,
local_path=local_path,
remote_path=remote_path,
destination_path=arguments['extract'].destination,
strip_components=arguments['extract'].strip_components,
progress=arguments['extract'].progress,
)
if 'export-tar' in arguments:
if arguments['export-tar'].repository is None or validate.repositories_match(
repository, arguments['export-tar'].repository
):
logger.info(
'{}: Exporting archive {} as tar file'.format(
repository, arguments['export-tar'].archive
)
)
borg_export_tar.export_tar_archive(
global_arguments.dry_run,
repository,
borg_list.resolve_archive_name(
repository, arguments['export-tar'].archive, storage, local_path, remote_path
),
arguments['export-tar'].paths,
arguments['export-tar'].destination,
storage,
local_path=local_path,
remote_path=remote_path,
tar_filter=arguments['export-tar'].tar_filter,
files=arguments['export-tar'].files,
strip_components=arguments['export-tar'].strip_components,
)
if 'mount' in arguments:
if arguments['mount'].repository is None or validate.repositories_match(
repository, arguments['mount'].repository
@ -553,7 +399,6 @@ def run_actions(
paths=dump.convert_glob_patterns_to_borg_patterns([dump_pattern]),
location_config=location,
storage_config=storage,
local_borg_version=local_borg_version,
local_path=local_path,
remote_path=remote_path,
destination_path='/',
@ -631,22 +476,6 @@ def run_actions(
)
if json_output:
yield json.loads(json_output)
if 'borg' in arguments:
if arguments['borg'].repository is None or validate.repositories_match(
repository, arguments['borg'].repository
):
logger.warning('{}: Running arbitrary Borg command'.format(repository))
archive_name = borg_list.resolve_archive_name(
repository, arguments['borg'].archive, storage, local_path, remote_path
)
borg_borg.run_arbitrary_borg(
repository,
storage,
options=arguments['borg'].options,
archive=archive_name,
local_path=local_path,
remote_path=remote_path,
)
def load_configurations(config_filenames, overrides=None):
@ -665,20 +494,6 @@ def load_configurations(config_filenames, overrides=None):
configs[config_filename] = validate.parse_configuration(
config_filename, validate.schema_filename(), overrides
)
except PermissionError:
logs.extend(
[
logging.makeLogRecord(
dict(
levelno=logging.WARNING,
levelname='WARNING',
msg='{}: Insufficient permissions to read configuration file'.format(
config_filename
),
)
),
]
)
except (ValueError, OSError, validate.Validation_error) as error:
logs.extend(
[
@ -711,39 +526,28 @@ def log_record(suppress_log=False, **kwargs):
return record
def log_error_records(
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
):
def make_error_log_records(message, error=None):
'''
Given error message text, an optional exception object, an optional log level, and whether to
log the error output of a CalledProcessError (if any), log error summary information and also
yield it as a series of logging.LogRecord instances.
Note that because the logs are yielded as a generator, logs won't get logged unless you consume
the generator output.
Given error message text and an optional exception object, yield a series of logging.LogRecord
instances with error summary information. As a side effect, log each record.
'''
level_name = logging._levelToName[levelno]
if not error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
return
try:
raise error
except CalledProcessError as error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
if error.output:
# Suppress these logs for now and save full error output for the log summary at the end.
yield log_record(
levelno=levelno,
levelname=level_name,
msg=error.output,
suppress_log=not log_command_error_output,
levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output, suppress_log=True
)
yield log_record(levelno=levelno, levelname=level_name, msg=error)
yield log_record(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
except (ValueError, OSError) as error:
yield log_record(levelno=levelno, levelname=level_name, msg=message)
yield log_record(levelno=levelno, levelname=level_name, 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.
@ -782,14 +586,12 @@ def collect_configuration_run_summary_logs(configs, arguments):
try:
validate.guard_configuration_contains_repository(repository, configs)
except ValueError as error:
yield from log_error_records(str(error))
yield from make_error_log_records(str(error))
return
if not configs:
yield from log_error_records(
'{}: No valid configuration files found'.format(
' '.join(arguments['global'].config_paths)
)
yield from make_error_log_records(
'{}: No configuration files found'.format(' '.join(arguments['global'].config_paths))
)
return
@ -805,7 +607,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
arguments['global'].dry_run,
)
except (CalledProcessError, ValueError, OSError) as error:
yield from log_error_records('Error running pre-everything hook', error)
yield from make_error_log_records('Error running pre-everything hook', error)
return
# Execute the actions corresponding to each configuration file.
@ -815,7 +617,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
if error_logs:
yield from log_error_records(
yield from make_error_log_records(
'{}: Error running configuration file'.format(config_filename)
)
yield from error_logs
@ -837,7 +639,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs)
)
except (CalledProcessError, OSError) as error:
yield from log_error_records('Error unmounting mount point', error)
yield from make_error_log_records('Error unmounting mount point', error)
if json_results:
sys.stdout.write(json.dumps(json_results))
@ -854,7 +656,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
arguments['global'].dry_run,
)
except (CalledProcessError, ValueError, OSError) as error:
yield from log_error_records('Error running post-everything hook', error)
yield from make_error_log_records('Error running post-everything hook', error)
def exit_with_help_link(): # pragma: no cover

View File

@ -99,9 +99,7 @@ def main(): # pragma: no cover
)
generate.write_configuration(
args.destination_config_filename,
generate.render_configuration(destination_config),
mode=source_config_file_mode,
args.destination_config_filename, destination_config, mode=source_config_file_mode
)
display_result(args)

View File

@ -44,9 +44,6 @@ def collect_config_filenames(config_paths):
yield path
continue
if not os.access(path, os.R_OK):
continue
for filename in sorted(os.listdir(path)):
full_filename = os.path.join(path, filename)
matching_filetype = full_filename.endswith('.yaml') or full_filename.endswith('.yml')

View File

@ -17,7 +17,7 @@ def _convert_section(source_section_config, section_schema):
(
option_name,
int(option_value)
if section_schema['properties'].get(option_name, {}).get('type') == 'integer'
if section_schema['map'].get(option_name, {}).get('type') == 'int'
else option_value,
)
for option_name, option_value in source_section_config.items()
@ -38,7 +38,7 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
'''
destination_config = yaml.comments.CommentedMap(
[
(section_name, _convert_section(section_config, schema['properties'][section_name]))
(section_name, _convert_section(section_config, schema['map'][section_name]))
for section_name, section_config in source_config._asdict().items()
]
)
@ -54,11 +54,11 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
# Add comments to each section, and then add comments to the fields in each section.
generate.add_comments_to_configuration_object(destination_config, schema)
generate.add_comments_to_configuration_map(destination_config, schema)
for section_name, section_config in destination_config.items():
generate.add_comments_to_configuration_object(
section_config, schema['properties'][section_name], indent=generate.INDENT
generate.add_comments_to_configuration_map(
section_config, schema['map'][section_name], indent=generate.INDENT
)
return destination_config

View File

@ -24,27 +24,31 @@ def _insert_newline_before_comment(config, field_name):
def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
'''
Given a loaded configuration schema, generate and return sample config for it. Include comments
for each section based on the schema "description".
for each section based on the schema "desc" description.
'''
schema_type = schema.get('type')
example = schema.get('example')
if example is not None:
return example
if schema_type == 'array':
if 'seq' in schema:
config = yaml.comments.CommentedSeq(
[_schema_to_sample_configuration(schema['items'], level, parent_is_sequence=True)]
[
_schema_to_sample_configuration(item_schema, level, parent_is_sequence=True)
for item_schema in schema['seq']
]
)
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
elif schema_type == 'object':
add_comments_to_configuration_sequence(
config, schema, indent=(level * INDENT) + SEQUENCE_INDENT
)
elif 'map' in schema:
config = yaml.comments.CommentedMap(
[
(field_name, _schema_to_sample_configuration(sub_schema, level + 1))
for field_name, sub_schema in schema['properties'].items()
for field_name, sub_schema in schema['map'].items()
]
)
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
add_comments_to_configuration_object(
add_comments_to_configuration_map(
config, schema, indent=indent, skip_first=parent_is_sequence
)
else:
@ -82,8 +86,8 @@ def _comment_out_optional_configuration(rendered_config):
optional = False
for line in rendered_config.split('\n'):
# Upon encountering an optional configuration option, comment out lines until the next blank
# line.
# Upon encountering an optional configuration option, commenting out lines until the next
# blank line.
if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
optional = True
continue
@ -97,7 +101,7 @@ def _comment_out_optional_configuration(rendered_config):
return '\n'.join(lines)
def render_configuration(config):
def _render_configuration(config):
'''
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
'''
@ -130,26 +134,26 @@ def write_configuration(config_filename, rendered_config, mode=0o600):
def add_comments_to_configuration_sequence(config, schema, indent=0):
'''
If the given config sequence's items are object, then mine the schema for the description of the
object's first item, and slap that atop the sequence. Indent the comment the given number of
If the given config sequence's items are maps, then mine the schema for the description of the
map's first item, and slap that atop the sequence. Indent the comment the given number of
characters.
Doing this for sequences of maps results in nice comments that look like:
```
things:
# First key description. Added by this function.
# First key description. Added by this function.
- key: foo
# Second key description. Added by add_comments_to_configuration_object().
# Second key description. Added by add_comments_to_configuration_map().
other: bar
```
'''
if schema['items'].get('type') != 'object':
if 'map' not in schema['seq'][0]:
return
for field_name in config[0].keys():
field_schema = schema['items']['properties'].get(field_name, {})
description = field_schema.get('description')
field_schema = schema['seq'][0]['map'].get(field_name, {})
description = field_schema.get('desc')
# No description to use? Skip it.
if not field_schema or not description:
@ -158,7 +162,7 @@ def add_comments_to_configuration_sequence(config, schema, indent=0):
config[0].yaml_set_start_comment(description, indent=indent)
# We only want the first key's description here, as the rest of the keys get commented by
# add_comments_to_configuration_object().
# add_comments_to_configuration_map().
return
@ -167,7 +171,7 @@ REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
COMMENTED_OUT_SENTINEL = 'COMMENT_OUT'
def add_comments_to_configuration_object(config, schema, indent=0, skip_first=False):
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
'''
Using descriptions from a schema as a source, add those descriptions as comments to the given
config mapping, before each field. Indent the comment the given number of characters.
@ -176,8 +180,8 @@ def add_comments_to_configuration_object(config, schema, indent=0, skip_first=Fa
if skip_first and index == 0:
continue
field_schema = schema['properties'].get(field_name, {})
description = field_schema.get('description', '').strip()
field_schema = schema['map'].get(field_name, {})
description = field_schema.get('desc', '').strip()
# If this is an optional key, add an indicator to the comment flagging it to be commented
# out from the sample configuration. This sentinel is consumed by downstream processing that
@ -266,9 +270,9 @@ def merge_source_configuration_into_destination(destination_config, source_confi
def generate_sample_configuration(source_filename, destination_filename, schema_filename):
'''
Given an optional source configuration filename, and a required destination configuration
filename, and the path to a schema filename in a YAML rendition of the JSON Schema format,
write out a sample configuration file based on that schema. If a source filename is provided,
merge the parsed contents of that configuration into the generated configuration.
filename, and the path to a schema filename in pykwalify YAML schema format, write out a
sample configuration file based on that schema. If a source filename is provided, merge the
parsed contents of that configuration into the generated configuration.
'''
schema = yaml.round_trip_load(open(schema_filename))
source_config = None
@ -282,5 +286,5 @@ def generate_sample_configuration(source_filename, destination_filename, schema_
write_configuration(
destination_filename,
_comment_out_optional_configuration(render_configuration(destination_config)),
_comment_out_optional_configuration(_render_configuration(destination_config)),
)

View File

@ -26,8 +26,6 @@ def convert_value_type(value):
'''
Given a string value, determine its logical type (string, boolean, integer, etc.), and return it
converted to that type.
Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
'''
return ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
@ -60,8 +58,6 @@ def parse_overrides(raw_overrides):
)
except ValueError:
raise ValueError('Invalid override. Make sure you use the form: SECTION.OPTION=VALUE')
except ruamel.yaml.error.YAMLError as error:
raise ValueError(f'Invalid override value: {error}')
def apply_overrides(config, raw_overrides):

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
import logging
import os
import jsonschema
import pkg_resources
import pykwalify.core
import pykwalify.errors
import ruamel.yaml
from borgmatic.config import load, normalize, override
@ -15,40 +17,15 @@ def schema_filename():
return pkg_resources.resource_filename('borgmatic', 'config/schema.yaml')
def format_json_error_path_element(path_element):
'''
Given a path element into a JSON data structure, format it for display as a string.
'''
if isinstance(path_element, int):
return str('[{}]'.format(path_element))
return str('.{}'.format(path_element))
def format_json_error(error):
'''
Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
'''
if not error.path:
return 'At the top level: {}'.format(error.message)
formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
return "At '{}': {}".format(formatted_path.lstrip('.'), error.message)
class Validation_error(ValueError):
'''
A collection of error messages generated when attempting to validate a particular
configuration file.
A collection of error message strings generated when attempting to validate a particular
configurartion file.
'''
def __init__(self, config_filename, errors):
'''
Given a configuration filename path and a sequence of string error messages, create a
Validation_error.
'''
def __init__(self, config_filename, error_messages):
self.config_filename = config_filename
self.errors = errors
self.error_messages = error_messages
def __str__(self):
'''
@ -56,7 +33,7 @@ class Validation_error(ValueError):
'''
return 'An error occurred while parsing a configuration file at {}:\n'.format(
self.config_filename
) + '\n'.join(error for error in self.errors)
) + '\n'.join(self.error_messages)
def apply_logical_validation(config_filename, parsed_configuration):
@ -88,12 +65,29 @@ def apply_logical_validation(config_filename, parsed_configuration):
)
def remove_examples(schema):
'''
pykwalify gets angry if the example field is not a string. So rather than bend to its will,
remove all examples from the given schema before passing the schema to pykwalify.
'''
if 'map' in schema:
for item_name, item_schema in schema['map'].items():
item_schema.pop('example', None)
remove_examples(item_schema)
elif 'seq' in schema:
for item_schema in schema['seq']:
item_schema.pop('example', None)
remove_examples(item_schema)
return schema
def parse_configuration(config_filename, schema_filename, overrides=None):
'''
Given the path to a config filename in YAML format, the path to a schema filename in a YAML
rendition of JSON Schema format, a sequence of configuration file override strings in the form
of "section.option=value", return the parsed configuration as a data structure of nested dicts
and lists corresponding to the schema. Example return value:
Given the path to a config filename in YAML format, the path to a schema filename in pykwalify
YAML schema format, a sequence of configuration file override strings in the form of
"section.option=value", return the parsed configuration as a data structure of nested dicts and
lists corresponding to the schema. Example return value:
{'location': {'source_directories': ['/home', '/etc'], 'repository': 'hostname.borg'},
'retention': {'keep_daily': 7}, 'consistency': {'checks': ['repository', 'archives']}}
@ -101,6 +95,8 @@ def parse_configuration(config_filename, schema_filename, overrides=None):
Raise FileNotFoundError if the file does not exist, PermissionError if the user does not
have permissions to read the file, or Validation_error if the config does not match the schema.
'''
logging.getLogger('pykwalify').setLevel(logging.ERROR)
try:
config = load.load_configuration(config_filename)
schema = load.load_configuration(schema_filename)
@ -110,20 +106,15 @@ def parse_configuration(config_filename, schema_filename, overrides=None):
override.apply_overrides(config, overrides)
normalize.normalize(config)
try:
validator = jsonschema.Draft7Validator(schema)
except AttributeError: # pragma: no cover
validator = jsonschema.Draft4Validator(schema)
validation_errors = tuple(validator.iter_errors(config))
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
parsed_result = validator.validate(raise_exception=False)
if validation_errors:
raise Validation_error(
config_filename, tuple(format_json_error(error) for error in validation_errors)
)
if validator.validation_errors:
raise Validation_error(config_filename, validator.validation_errors)
apply_logical_validation(config_filename, config)
apply_logical_validation(config_filename, parsed_result)
return config
return parsed_result
def normalize_repository_path(repository):

View File

@ -23,7 +23,7 @@ def exit_code_indicates_error(process, exit_code, borg_local_path=None):
command = process.args.split(' ') if isinstance(process.args, str) else process.args
if borg_local_path and command[0] == borg_local_path:
return bool(exit_code < 0 or exit_code >= BORG_ERROR_EXIT_CODE)
return bool(exit_code >= BORG_ERROR_EXIT_CODE)
return bool(exit_code != 0)
@ -59,12 +59,11 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
'''
# Map from output buffer to sequence of last lines.
buffer_last_lines = collections.defaultdict(list)
process_for_output_buffer = {
output_buffer_for_process(process, exclude_stdouts): process
output_buffers = [
output_buffer_for_process(process, exclude_stdouts)
for process in processes
if process.stdout or process.stderr
}
output_buffers = list(process_for_output_buffer.keys())
]
# Log output for each process until they all exit.
while True:
@ -72,23 +71,8 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
(ready_buffers, _, _) = select.select(output_buffers, [], [])
for ready_buffer in ready_buffers:
ready_process = process_for_output_buffer.get(ready_buffer)
# The "ready" process has exited, but it might be a pipe destination with other
# processes (pipe sources) waiting to be read from. So as a measure to prevent
# hangs, vent all processes when one exits.
if ready_process and ready_process.poll() is not None:
for other_process in processes:
if (
other_process.poll() is None
and other_process.stdout
and other_process.stdout not in output_buffers
):
# Add the process's output to output_buffers to ensure it'll get read.
output_buffers.append(other_process.stdout)
line = ready_buffer.readline().rstrip().decode()
if not line or not ready_process:
if not line:
continue
# Keep the last few lines of output in case the process errors, and we need the output for
@ -100,14 +84,9 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
logger.log(output_log_level, line)
still_running = False
for process in processes:
exit_code = process.poll() if output_buffers else process.wait()
if exit_code is None:
still_running = True
# If any process errors, then raise accordingly.
if exit_code_indicates_error(process, exit_code, borg_local_path):
# If an error occurs, include its output in the raised exception so that we don't
@ -129,7 +108,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
exit_code, command_for_process(process), '\n'.join(last_lines)
)
if not still_running:
if all(process.poll() is not None for process in processes):
break
# Consume any remaining output that we missed (if any).
@ -139,12 +118,9 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
if not output_buffer:
continue
while True: # pragma: no cover
remaining_output = output_buffer.readline().rstrip().decode()
if not remaining_output:
break
remaining_output = output_buffer.read().rstrip().decode()
if remaining_output: # pragma: no cover
logger.log(output_log_level, remaining_output)

View File

@ -13,15 +13,6 @@ MONITOR_STATE_TO_CRONHUB = {
}
def initialize_monitor(
ping_url, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the given Cronhub URL, modified with the monitor.State. Use the given configuration
@ -39,12 +30,3 @@ def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.get(ping_url)
def destroy_monitor(
ping_url_or_uuid, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass

View File

@ -13,15 +13,6 @@ MONITOR_STATE_TO_CRONITOR = {
}
def initialize_monitor(
ping_url, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the given Cronitor URL, modified with the monitor.State. Use the given configuration
@ -38,12 +29,3 @@ def ping_monitor(ping_url, config_filename, state, monitoring_log_level, dry_run
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.get(ping_url)
def destroy_monitor(
ping_url_or_uuid, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass

View File

@ -1,6 +1,6 @@
import logging
from borgmatic.hooks import cronhub, cronitor, healthchecks, mongodb, mysql, pagerduty, postgresql
from borgmatic.hooks import cronhub, cronitor, healthchecks, mysql, pagerduty, postgresql
logger = logging.getLogger(__name__)
@ -11,7 +11,6 @@ HOOK_NAME_TO_MODULE = {
'pagerduty': pagerduty,
'postgresql_databases': postgresql,
'mysql_databases': mysql,
'mongodb_databases': mongodb,
}
@ -59,5 +58,5 @@ def call_hooks(function_name, hooks, log_prefix, hook_names, *args, **kwargs):
return {
hook_name: call_hook(function_name, hooks, log_prefix, hook_name, *args, **kwargs)
for hook_name in hook_names
if hooks.get(hook_name)
if hook_name in hooks
}

View File

@ -6,7 +6,7 @@ from borgmatic.borg.create import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__)
DATABASE_HOOK_NAMES = ('postgresql_databases', 'mysql_databases', 'mongodb_databases')
DATABASE_HOOK_NAMES = ('postgresql_databases', 'mysql_databases')
def make_database_dump_path(borgmatic_source_directory, database_hook_name):
@ -48,24 +48,46 @@ def create_named_pipe_for_dump(dump_path):
os.mkfifo(dump_path, mode=0o600)
def remove_database_dumps(dump_path, database_type_name, log_prefix, dry_run):
def remove_database_dumps(dump_path, databases, database_type_name, log_prefix, dry_run):
'''
Remove all database dumps in the given dump directory path (including the directory itself). If
this is a dry run, then don't actually remove anything.
Remove the database dumps for the given databases in the dump directory path. The databases are
supplied as a sequence of dicts, one dict describing each database as per the configuration
schema. Use the name of the database type and the log prefix in any log entries. If this is a
dry run, then don't actually remove anything.
'''
if not databases:
logger.debug('{}: No {} databases configured'.format(log_prefix, database_type_name))
return
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
logger.info(
'{}: Removing {} database dumps{}'.format(log_prefix, database_type_name, dry_run_label)
)
expanded_path = os.path.expanduser(dump_path)
for database in databases:
dump_filename = make_database_dump_filename(
dump_path, database['name'], database.get('hostname')
)
if dry_run:
return
logger.debug(
'{}: Removing {} database dump {} from {}{}'.format(
log_prefix, database_type_name, database['name'], dump_filename, dry_run_label
)
)
if dry_run:
continue
if os.path.exists(expanded_path):
shutil.rmtree(expanded_path)
if os.path.exists(dump_filename):
if os.path.isdir(dump_filename):
shutil.rmtree(dump_filename)
else:
os.remove(dump_filename)
dump_file_dir = os.path.dirname(dump_filename)
if os.path.exists(dump_file_dir) and len(os.listdir(dump_file_dir)) == 0:
os.rmdir(dump_file_dir)
def convert_glob_patterns_to_borg_patterns(patterns):

View File

@ -13,7 +13,7 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
}
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
PAYLOAD_LIMIT_BYTES = 100 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR)
PAYLOAD_LIMIT_BYTES = 10 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR)
class Forgetful_buffering_handler(logging.Handler):
@ -65,24 +65,20 @@ def format_buffered_logs_for_payload():
return payload
def initialize_monitor(
ping_url_or_uuid, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
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, monitoring_log_level)
)
def ping_monitor(ping_url_or_uuid, config_filename, state, monitoring_log_level, dry_run):
'''
Ping the given Healthchecks URL or UUID, modified with the monitor.State. Use the given
configuration filename in any log entries, and log to Healthchecks with the giving log level.
If this is a dry run, then don't actually ping anything.
'''
if state is monitor.State.START:
# Add a handler to the root logger that stores in memory the most recent logs emitted. That
# way, we can send them all to Healthchecks upon a finish or failure state.
logging.getLogger().addHandler(
Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES, monitoring_log_level)
)
payload = ''
ping_url = (
ping_url_or_uuid
if ping_url_or_uuid.startswith('http')
@ -101,21 +97,7 @@ def ping_monitor(ping_url_or_uuid, config_filename, state, monitoring_log_level,
if state in (monitor.State.FINISH, monitor.State.FAIL):
payload = format_buffered_logs_for_payload()
else:
payload = ''
if not dry_run:
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.post(ping_url, data=payload.encode('utf-8'))
def destroy_monitor(ping_url_or_uuid, config_filename, monitoring_log_level, dry_run):
'''
Remove the monitor handler that was added to the root logger. This prevents the handler from
getting reused by other instances of this monitor.
'''
logger = logging.getLogger()
for handler in tuple(logger.handlers):
if isinstance(handler, Forgetful_buffering_handler):
logger.removeHandler(handler)

View File

@ -1,162 +0,0 @@
import logging
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
def make_dump_path(location_config): # pragma: no cover
'''
Make the dump path from the given location configuration and the name of this hook.
'''
return dump.make_database_dump_path(
location_config.get('borgmatic_source_directory'), 'mongodb_databases'
)
def dump_databases(databases, log_prefix, location_config, dry_run):
'''
Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given log
prefix in any log entries. Use the given location configuration dict to construct the
destination path.
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
logger.info('{}: Dumping MongoDB databases{}'.format(log_prefix, dry_run_label))
processes = []
for database in databases:
name = database['name']
dump_filename = dump.make_database_dump_filename(
make_dump_path(location_config), name, database.get('hostname')
)
dump_format = database.get('format', 'archive')
logger.debug(
'{}: Dumping MongoDB database {} to {}{}'.format(
log_prefix, name, dump_filename, dry_run_label
)
)
if dry_run:
continue
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
else:
dump.create_named_pipe_for_dump(dump_filename)
command = build_dump_command(database, dump_filename, dump_format)
processes.append(execute_command(command, shell=True, run_to_completion=False))
return processes
def build_dump_command(database, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
all_databases = database['name'] == 'all'
command = ['mongodump', '--archive']
if dump_format == 'directory':
command.append(dump_filename)
if 'hostname' in database:
command.extend(('--host', database['hostname']))
if 'port' in database:
command.extend(('--port', str(database['port'])))
if 'username' in database:
command.extend(('--username', database['username']))
if 'password' in database:
command.extend(('--password', database['password']))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
if not all_databases:
command.extend(('--db', database['name']))
if 'options' in database:
command.extend(database['options'].split(' '))
if dump_format != 'directory':
command.extend(('>', dump_filename))
return command
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the log
prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually remove anything.
'''
dump.remove_database_dumps(make_dump_path(location_config), 'MongoDB', log_prefix, dry_run)
def make_database_dump_pattern(
databases, log_prefix, location_config, name=None
): # pragma: no cover
'''
Given a sequence of configurations dicts, a prefix to log with, a location configuration dict,
and a database name to match, return the corresponding glob patterns to match the database dump
in an archive.
'''
return dump.make_database_dump_filename(make_dump_path(location_config), name, hostname='*')
def restore_database_dump(database_config, log_prefix, location_config, dry_run, extract_process):
'''
Restore the given MongoDB database from an extract stream. The database is supplied as a
one-element sequence containing a dict describing the database, as per the configuration schema.
Use the given log prefix in any log entries. If this is a dry run, then don't actually restore
anything. Trigger the given active extract process (an instance of subprocess.Popen) to produce
output to consume.
If the extract process is None, then restore the dump from the filesystem rather than from an
extract stream.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
if len(database_config) != 1:
raise ValueError('The database configuration value is invalid')
database = database_config[0]
dump_filename = dump.make_database_dump_filename(
make_dump_path(location_config), database['name'], database.get('hostname')
)
restore_command = build_restore_command(extract_process, database, dump_filename)
logger.debug(
'{}: Restoring MongoDB database {}{}'.format(log_prefix, database['name'], dry_run_label)
)
if dry_run:
return
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
borg_local_path=location_config.get('local_path', 'borg'),
)
def build_restore_command(extract_process, database, dump_filename):
'''
Return the mongorestore command from a single database configuration.
'''
command = ['mongorestore', '--archive']
if not extract_process:
command.append(dump_filename)
if database['name'] != 'all':
command.extend(('--drop', '--db', database['name']))
if 'hostname' in database:
command.extend(('--host', database['hostname']))
if 'port' in database:
command.extend(('--port', str(database['port'])))
if 'username' in database:
command.extend(('--username', database['username']))
if 'password' in database:
command.extend(('--password', database['password']))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
return command

View File

@ -31,7 +31,6 @@ def database_names_to_dump(database, extra_environment, log_prefix, dry_run_labe
show_command = (
('mysql',)
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
@ -82,12 +81,12 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
dump_command = (
('mysqldump',)
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ ('--add-drop-database',)
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
+ (('--user', database['username']) if 'username' in database else ())
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
+ ('--databases',)
+ dump_database_names
# Use shell redirection rather than execute_command(output_file=open(...)) to prevent
@ -119,11 +118,14 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the log
prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually remove anything.
Remove the database dumps for the given databases. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the log prefix in
any log entries. Use the given location configuration dict to construct the destination path. If
this is a dry run, then don't actually remove anything.
'''
dump.remove_database_dumps(make_dump_path(location_config), 'MySQL', log_prefix, dry_run)
dump.remove_database_dumps(
make_dump_path(location_config), databases, 'MySQL', log_prefix, dry_run
)
def make_database_dump_pattern(
@ -152,7 +154,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
database = database_config[0]
restore_command = (
('mysql', '--batch')
('mysql', '--batch', '--verbose')
+ (('--host', database['hostname']) if 'hostname' in database else ())
+ (('--port', str(database['port'])) if 'port' in database else ())
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())

View File

@ -12,15 +12,6 @@ logger = logging.getLogger(__name__)
EVENTS_API_URL = 'https://events.pagerduty.com/v2/enqueue'
def initialize_monitor(
integration_key, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No initialization is necessary for this monitor.
'''
pass
def ping_monitor(integration_key, config_filename, state, monitoring_log_level, dry_run):
'''
If this is an error state, create a PagerDuty event with the given integration key. Use the
@ -69,12 +60,3 @@ def ping_monitor(integration_key, config_filename, state, monitoring_log_level,
logging.getLogger('urllib3').setLevel(logging.ERROR)
requests.post(EVENTS_API_URL, data=payload.encode('utf-8'))
def destroy_monitor(
ping_url_or_uuid, config_filename, monitoring_log_level, dry_run
): # pragma: no cover
'''
No destruction is necessary for this monitor.
'''
pass

View File

@ -15,25 +15,6 @@ def make_dump_path(location_config): # pragma: no cover
)
def make_extra_environment(database):
'''
Make the extra_environment dict from the given database configuration.
'''
extra = dict()
if 'password' in database:
extra['PGPASSWORD'] = database['password']
extra['PGSSLMODE'] = database.get('ssl_mode', 'disable')
if 'ssl_cert' in database:
extra['PGSSLCERT'] = database['ssl_cert']
if 'ssl_key' in database:
extra['PGSSLKEY'] = database['ssl_key']
if 'ssl_root_cert' in database:
extra['PGSSLROOTCERT'] = database['ssl_root_cert']
if 'ssl_crl' in database:
extra['PGSSLCRL'] = database['ssl_crl']
return extra
def dump_databases(databases, log_prefix, location_config, dry_run):
'''
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
@ -75,7 +56,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
# format in a particular, a named destination is required, and redirection doesn't work.
+ (('>', dump_filename) if dump_format != 'directory' else ())
)
extra_environment = make_extra_environment(database)
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
logger.debug(
'{}: Dumping PostgreSQL database {} to {}{}'.format(
@ -101,11 +82,14 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
'''
Remove all database dump files for this hook regardless of the given databases. Use the log
prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually remove anything.
Remove the database dumps for the given databases. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the log prefix in
any log entries. Use the given location configuration dict to construct the destination path. If
this is a dry run, then don't actually remove anything.
'''
dump.remove_database_dumps(make_dump_path(location_config), 'PostgreSQL', log_prefix, dry_run)
dump.remove_database_dumps(
make_dump_path(location_config), databases, 'PostgreSQL', log_prefix, dry_run
)
def make_database_dump_pattern(
@ -160,7 +144,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
+ (('--username', database['username']) if 'username' in database else ())
+ (() if extract_process else (dump_filename,))
)
extra_environment = make_extra_environment(database)
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
logger.debug(
'{}: Restoring PostgreSQL database {}{}'.format(log_prefix, database['name'], dry_run_label)

View File

@ -1,5 +1,4 @@
import logging
import logging.handlers
import os
import sys
@ -152,8 +151,6 @@ def configure_logging(
syslog_path = '/dev/log'
elif os.path.exists('/var/run/syslog'):
syslog_path = '/var/run/syslog'
elif os.path.exists('/var/run/log'):
syslog_path = '/var/run/log'
if syslog_path and not interactive_console():
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)

View File

@ -1,34 +1,18 @@
import logging
import os
import signal
import sys
logger = logging.getLogger(__name__)
EXIT_CODE_FROM_SIGNAL = 128
def handle_signal(signal_number, frame):
def _handle_signal(signal_number, frame): # pragma: no cover
'''
Send the signal to all processes in borgmatic's process group, which includes child processes.
Send the signal to all processes in borgmatic's process group, which includes child process.
'''
# Prevent infinite signal handler recursion. If the parent frame is this very same handler
# function, we know we're recursing.
if frame.f_back.f_code.co_name == handle_signal.__name__:
return
os.killpg(os.getpgrp(), signal_number)
if signal_number == signal.SIGTERM:
logger.critical('Exiting due to TERM signal')
sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
def configure_signals():
def configure_signals(): # pragma: no cover
'''
Configure borgmatic's signal handlers to pass relevant signals through to any child processes
like Borg. Note that SIGINT gets passed through even without these changes.
'''
for signal_number in (signal.SIGHUP, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2):
signal.signal(signal_number, handle_signal)
signal.signal(signal_number, _handle_signal)

View File

@ -1,14 +1,13 @@
FROM python:3.8-alpine3.13 as borgmatic
FROM python:3.8.1-alpine3.11 as borgmatic
COPY . /app
RUN apk add --no-cache py3-ruamel.yaml py3-ruamel.yaml.clib
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 compact create check extract export-tar mount umount restore list info borg; 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
FROM node:15.2.1-alpine as html
FROM node:13.7.0-alpine as html
ARG ENVIRONMENT=production
@ -17,7 +16,6 @@ WORKDIR /source
RUN npm install @11ty/eleventy \
@11ty/eleventy-plugin-syntaxhighlight \
@11ty/eleventy-plugin-inclusive-language \
@11ty/eleventy-navigation \
markdown-it \
markdown-it-anchor \
markdown-it-replace-link
@ -27,7 +25,7 @@ COPY . /source
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
&& mv /output/docs/index.html /output/index.html
FROM nginx:1.19.4-alpine
FROM nginx:1.16.1-alpine
COPY --from=html /output /usr/share/nginx/html
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml

View File

@ -1,7 +1,8 @@
/* Buzzwords */
@keyframes rainbow {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.buzzword-list,
.inlinelist {
@ -24,7 +25,6 @@
margin: 4px 4px 4px 0;
transition: .15s linear outline;
}
.inlinelist .inlinelist-item.active {
background-color: #222;
color: #fff;
@ -36,38 +36,6 @@
}
.inlinelist .inlinelist-item code {
background-color: transparent;
font-size: 80%;
margin-left: 6px;
padding-left: 6px;
display: inline-block;
position: relative;
}
@media (max-width: 26.8125em) { /* 429px */
.inlinelist .inlinelist-item {
overflow: hidden;
}
.inlinelist .inlinelist-item code {
float: right;
line-height: 1.75;
}
}
@media (min-width: 26.875em) { /* 430px */
.inlinelist .inlinelist-item code {
float: none;
}
.inlinelist .inlinelist-item code:before {
content: " ";
border-left: 1px solid rgba(255,255,255,.8);
position: absolute;
left: -2px;
top: -2px;
bottom: 2px;
}
@media (prefers-color-scheme: dark) {
.inlinelist .inlinelist-item code:before {
border-left-color: rgba(0,0,0,.8);
}
}
}
a.buzzword {
text-decoration: underline;
@ -91,74 +59,44 @@ a.buzzword {
.buzzword {
background-color: #f7f7f7;
}
@media (prefers-color-scheme: dark) {
.buzzword-list li,
.buzzword {
background-color: #080808;
}
}
.inlinelist .inlinelist-item {
background-color: #e9e9e9;
}
@media (prefers-color-scheme: dark) {
.inlinelist .inlinelist-item {
background-color: #000;
}
.inlinelist .inlinelist-item a {
color: #fff;
}
.inlinelist .inlinelist-item code {
color: inherit;
}
}
.inlinelist .inlinelist-item:hover,
.inlinelist .inlinelist-item:focus,
.buzzword-list li:hover,
.buzzword-list li:focus,
.buzzword:hover,
.buzzword:focus,
.rainbow-active:hover,
.rainbow-active:focus {
.buzzword:focus {
position: relative;
background-image: linear-gradient(238deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff0080);
background-size: 1200% 1200%;
background-position: 2% 80%;
color: #fff;
text-shadow: 0 0 2px rgba(0,0,0,.9);
animation: rainbow 4s ease-out alternate infinite;
}
.rainbow-active-noanim {
animation: none !important;
animation: rainbow 1.6s infinite;
}
.inlinelist .inlinelist-item:hover a,
.inlinelist .inlinelist-item:focus a,
.buzzword-list li:hover a,
.buzzword-list li:focus a,
a.buzzword:hover,
a.buzzword:focus,
a.rainbow-active:hover,
a.rainbow-active:focus {
a.buzzword:focus {
color: #fff;
text-decoration: none;
}
@media (prefers-reduced-motion: reduce) {
.inlinelist .inlinelist-item:hover,
.inlinelist .inlinelist-item:focus,
.buzzword-list li:hover,
.buzzword-list li:focus,
/*
I wish there were a PE friendly way to do this but media queries dont work work with @supports
@media (prefers-reduced-motion: no-preference) {
.buzzword:hover,
.buzzword:focus,
.rainbow-active:hover,
.rainbow-active:focus {
animation: none;
.buzzword:focus {
animation: rainbow 1s infinite;
}
}
}*/
.buzzword-list li:hover:after,
.buzzword-list li:focus:after,
.buzzword:hover:after,
.buzzword:focus:after {
font-family: system-ui, -apple-system, sans-serif;
font-family: system-ui, sans-serif;
content: "Buzzword alert!!!";
position: absolute;
left: 0;
@ -185,94 +123,4 @@ main h2 a.buzzword,
main h3 a.buzzword,
main p a.buzzword {
text-decoration: underline;
}
/* Small viewport */
@media (max-width: 26.8125em) { /* 429px */
.inlinelist .inlinelist-item {
display: block;
width: auto;
padding: 0;
line-height: 1.4;
}
.inlinelist .inlinelist-item > a {
display: block;
padding: .2em .5em;
}
}
@media (min-width: 26.875em) { /* 430px */
.inlinelist .inlinelist-item > a {
display: inline-block;
white-space: nowrap;
}
}
.numberflag {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: #dff7ff;
border-radius: 50%;
width: 1.75em;
height: 1.75em;
font-weight: 600;
}
@media (prefers-color-scheme: dark) {
.numberflag {
background-color: #00bcd4;
color: #222;
}
}
h1 .numberflag,
h2 .numberflag,
h3 .numberflag,
h4 .numberflag,
h5 .numberflag {
width: 1.25em;
height: 1.25em;
}
h2 .numberflag {
position: relative;
margin-right: 0.25em; /* 10px /40 */
}
h2 .numberflag:after {
content: " ";
position: absolute;
bottom: -1px;
left: 0;
height: 1px;
background-color: #fff;
width: calc(100% + 0.4em); /* 16px /40 */
}
@media (prefers-color-scheme: dark) {
h2 .numberflag:after {
background-color: #222;
}
}
/* Super featured list on home page */
.list-superfeatured .avatar {
width: calc(30px + 5vw);
height: calc(30px + 5vw);
max-width: 60px;
max-height: 60px;
margin-left: 0;
}
@media (max-width: 26.8125em) { /* 429px */
.list-superfeatured .inlinelist-item > a {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
@media (min-width: 26.875em) { /* 430px */
.list-superfeatured .inlinelist-item {
font-size: 110%;
}
}
/* Only top level */
.inlinelist-no-nest ul,
.inlinelist-no-nest ol {
display: none;
}
}

View File

@ -10,20 +10,7 @@
font-weight: 500;
margin: 0 0.4285714285714em 0.07142857142857em 0; /* 0 6px 1px 0 /14 */
line-height: 1.285714285714; /* 18px /14 */
font-family: system-ui, -apple-system, sans-serif;
}
@media (prefers-color-scheme: dark) {
.minilink {
background-color: #222;
/*
!important to override .elv-callout a
see _includes/components/callout.css
*/
color: #fff !important;
}
}
table .minilink {
margin-top: 6px;
font-family: system-ui, sans-serif;
}
.minilink[href] {
box-shadow: 0 1px 1px 0 rgba(0,0,0,.5);
@ -32,12 +19,6 @@ table .minilink {
.minilink[href]:focus {
background-color: #bbb;
}
@media (prefers-color-scheme: dark) {
.minilink[href]:hover,
.minilink[href]:focus {
background-color: #444;
}
}
pre + .minilink {
color: #fff;
border-radius: 0 0 0.2857142857143em 0.2857142857143em; /* 4px /14 */
@ -54,54 +35,6 @@ p.minilink {
margin-left: 2em;
margin-bottom: 2em;
}
h1 .minilink,
h2 .minilink,
h3 .minilink,
h4 .minilink {
font-size: 0.9375rem; /* 15px /16 */
vertical-align: middle;
margin-left: 1em;
}
h3 .minilink,
h4 .minilink {
font-size: 0.8125rem; /* 13px /16 */
}
.minilink + pre[class*=language-] {
clear: both;
}
.minilink-addedin {
text-transform: none;
box-shadow: 0 0 0 1px rgba(0,0,0,0.3);
}
@media (prefers-color-scheme: dark) {
.minilink-addedin {
box-shadow: 0 0 0 1px rgba(255,255,255,0.3);
}
}
.minilink-addedin:not(:first-child) {
margin-left: .5em;
}
.minilink-addedin.minilink-inline {
margin: 0 4px;
background-color: #fff;
}
.minilink-lower {
text-transform: none;
background-color: transparent;
}
.minilink-lower[href] {
box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
}
.minilink-lower[href]:hover,
.minilink-lower[href]:focus {
background-color: #eee;
}
.minilink > .minilink {
margin: -.125em .375em -.125em -.375em;
box-shadow: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}

View File

@ -0,0 +1,18 @@
#suggestion-form textarea {
font-family: sans-serif;
width: 100%;
}
#suggestion-form label {
font-weight: bold;
}
#suggestion-form input[type=email] {
font-size: 16px;
width: 100%;
}
#suggestion-form .form-error {
color: red;
}

View File

@ -0,0 +1,33 @@
<h2>Improve this documentation</h2>
<p>Have an idea on how to make this documentation even better? Send your
feedback below! But if you need help with borgmatic, or have an idea for a
borgmatic feature, please use our <a href="https://torsion.org/borgmatic/#issues">issue
tracker</a> instead.</p>
<form id="suggestion-form">
<div><label for="suggestion">Documentation suggestion</label></div>
<textarea id="suggestion" rows="8" cols="60" name="suggestion"></textarea>
<div data-sk-error="suggestion" class="form-error"></div>
<input id="_page" type="hidden" name="_page">
<input id="_subject" type="hidden" name="_subject" value="borgmatic documentation suggestion">
<br />
<label for="email">Email address</label>
<div><input id="email" type="email" name="email" placeholder="Only required if you want a response!"></div>
<div data-sk-error="email" class="form-error"></div>
<br />
<div><button type="submit">Send</button></div>
<br />
</form>
<script>
document.getElementById('_page').value = window.location.href;
window.sk=window.sk||function(){(sk.q=sk.q||[]).push(arguments)};
sk('form', 'init', {
id: '1d536680ab96',
element: '#suggestion-form'
});
</script>
<script defer src="https://js.statickit.com/statickit.js"></script>

View File

@ -1,5 +0,0 @@
<h2>Improve this documentation</h2>
<p>Have an idea on how to make this documentation even better? Use our <a
href="https://projects.torsion.org/borgmatic-collective/borgmatic/issues">issue tracker</a> to send your
feedback!</p>

View File

@ -1,111 +1,63 @@
.elv-toc {
font-size: 1rem; /* Reset */
}
.elv-toc details {
--details-force-closed: (max-width: 63.9375em); /* 1023px */
}
.elv-toc details > summary {
font-size: 1.375rem; /* 22px /16 */
margin-bottom: .5em;
}
@media (min-width: 64em) { /* 1024px */
.elv-toc {
position: absolute;
left: 3rem;
left: -17rem;
width: 16rem;
z-index: 1;
}
.elv-toc details > summary {
margin-top: 0;
}
.js .elv-toc details > summary {
display: none;
}
}
.elv-toc-list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding-left: 0;
padding-right: 0;
margin: 0 0 2.5em;
list-style: none;
}
.elv-toc-list li {
font-size: 0.9375em; /* 15px /16 */
line-height: 1.466666666667; /* 22px /15 */
}
/* Nested lists */
.elv-toc-list ul {
padding: 0 0 .75em 0;
margin: 0;
padding: 0;
display: none;
margin-bottom: 1.5em;
list-style: none;
}
/* Menus nested 2 or more deep */
.elv-toc-list ul ul {
padding-bottom: 0;
padding-left: 0.625rem; /* 10px /16 */
.elv-toc-list ul li {
padding-left: 0.875em; /* 14px /16 */
}
/* Hide inactive menus 3 or more deep */
.elv-toc-list ul ul > li:not(.elv-toc-active) > ul > li:not(.elv-toc-active) {
display: none;
@media (min-width: 64em) and (min-height: 48em) { /* 1024 x 768px */
.elv-toc-list ul {
display: block;
}
}
/* List items */
.elv-toc summary,
.elv-toc-list a {
padding: .15em .25em;
}
.elv-toc-list a {
display: block;
}
.elv-toc-list a:not(:hover) {
text-decoration: none;
}
.elv-toc-list li {
margin: 0;
padding: 0;
}
.elv-toc-list > li {
flex-grow: 1;
flex-basis: 14.375rem; /* 230px /16 */
padding-top: 0;
padding-bottom: 0;
margin: .1em 0 .5em;
}
/* Top level links */
.elv-toc-list > li > a {
font-weight: 400;
font-size: 1.0625em; /* 17px /16 */
color: #222;
font-weight: 600;
border-bottom: 1px solid #ddd;
margin-bottom: 0.25em; /* 4px /16 */
}
@media (prefers-color-scheme: dark) {
.elv-toc-list > li > a {
color: #fff;
border-color: #444;
}
}
/* Active links */
.elv-toc-list li.elv-toc-active > a {
background-color: #dff7ff;
font-weight: 700;
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
.elv-toc-list li.elv-toc-active > a {
background-color: #353535;
}
.elv-toc-active > a:after {
content: " ⬅";
line-height: .5;
}
.elv-toc-list ul .elv-toc-active > a:after {
content: "";
}
/* Show only active nested lists */
.elv-toc-list ul.elv-toc-active,
.elv-toc-list li.elv-toc-active > ul {
display: block;
}
/* Footer catgory navigation */
.elv-cat-list-active {
font-weight: 600;
}
}

View File

@ -181,7 +181,7 @@ pre {
padding: .5em;
margin: 1em -.5em 2em -.5em;
overflow-x: auto;
background-color: #fafafa;
background-color: #eee;
font-size: 0.75em; /* 12px /16 */
}
pre,
@ -194,7 +194,7 @@ code {
-webkit-hyphens: manual;
-moz-hyphens: manual;
hyphens: manual;
background-color: #fafafa;
background-color: #efefef;
}
pre + pre[class*="language-"] {
margin-top: 1em;
@ -234,9 +234,6 @@ pre + .note {
max-width: 42rem;
clear: both;
}
header.elv-layout {
padding: 0 1rem;
}
footer.elv-layout {
margin-bottom: 5em;
}
@ -245,7 +242,7 @@ footer.elv-layout {
}
@media (min-width: 64em) { /* 1024px */
.elv-layout-toc {
padding-left: 15rem;
margin-left: 18rem;
max-width: 60rem;
margin-right: 1rem;
position: relative;
@ -257,21 +254,14 @@ footer.elv-layout {
/* Header */
.elv-header {
color: #222;
position: relative;
text-align: center;
}
.elv-header-default {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 0;
}
.elv-header-c {
width: 100%;
}
.elv-header-docs .elv-header-c {
padding: 1rem 0;
}
.elv-header-docs:before,
.elv-header-docs:after {
@ -282,89 +272,53 @@ footer.elv-layout {
clear: both;
}
/* Header Hero */
.elv-hero {
background-color: #222;
.elv-hero img {
max-width: 80vw;
max-height: 60vh;
}
@media (prefers-color-scheme: dark) {
.elv-hero {
background-color: #292929;
}
}
.elv-hero img,
.elv-hero svg {
width: 42.95774646vh;
height: 60vh;
}
.elv-hero:hover img,
.elv-hero:hover svg {
background-color: inherit;
}
.elv-header-default .elv-hero {
display: flex;
justify-content: center;
width: calc(100% + 2rem);
margin-left: -1rem;
margin-right: -1rem;
}
.elv-hero:hover {
background-color: #333;
}
.elv-header-docs .elv-hero {
float: left;
margin-right: .5em;
margin-right: 1.5em;
}
.elv-header-default .elv-hero img,
.elv-header-default .elv-hero svg {
position: relative;
background-color: transparent;
z-index: 1;
}
.elv-header-docs .elv-hero img,
.elv-header-docs .elv-hero svg {
width: auto;
.elv-header-docs .elv-hero img {
height: 3em;
}
@media (min-width: 43.75em) { /* 700px */
.elv-header-docs .elv-hero {
margin-right: 1em;
}
.elv-header-docs .elv-hero img,
.elv-header-docs .elv-hero svg {
@media (min-width: 37.5em) { /* 600px */
.elv-header-docs .elv-hero img {
width: 4.303125em; /* 68.85px /16 */
height: 6em;
}
}
/* Header Possum */
.elv-possum-anchor {
display: block;
}
.elv-possum {
display: none;
position: absolute;
right: .5rem;
top: 1rem;
transition: .3s opacity ease-out;
right: 1em;
top: 1em;
width: 16vmin;
}
.elv-header-docs .elv-possum {
width: 15vw;
max-width: 6.25rem; /* 100px /16 */
@media (min-width: 31.25em) { /* 500px */
.elv-possum {
display: block;
}
}
.elv-header-default {
overflow: hidden;
/* Header Heading */
.elv-hed {
font-size: 3em;
margin-top: 1.5em;
margin-bottom: .25em;
text-align: center;
text-transform: none;
}
.elv-header-default .elv-possum {
pointer-events: none;
width: auto;
height: calc((60vh - 2rem) / 1.6);
top: 36%;
left: 1vw;
right: auto;
animation-duration: 180s;
animation-name: balloonFloat;
.elv-header-docs .elv-hed {
font-size: 2.3em;
margin: 0;
text-align: left;
}
@media (prefers-reduced-motion: reduce) {
.elv-header-default .elv-possum {
display: none;
@media (min-width: 37.5em) { /* 600px */
.elv-header-docs .elv-hed {
font-size: 3em;
}
}

View File

@ -11,6 +11,7 @@
{% include 'components/minilink.css' %}
{% include 'components/toc.css' %}
{% include 'components/info-blocks.css' %}
{% include 'components/suggestion-form.css' %}
{% include 'prism-theme.css' %}
{% include 'asciinema.css' %}
{% endset %}

View File

@ -6,27 +6,9 @@ headerClass: elv-header-default
{% include "header.njk" %}
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
<div id="documentation" class="elv-toc">
<div>
{% set navPages = collections.all | eleventyNavigation %}
{% macro renderNavListItem(entry) -%}
<li{% if entry.url == page.url %} class="elv-toc-active"{% endif %}>
<a {% if entry.url %}href="https://torsion.org/borgmatic/docs{{ entry.url | url }}"{% endif %}>{{ entry.title }}</a>
{%- if entry.children.length -%}
<ul>
{%- for child in entry.children %}{{ renderNavListItem(child) }}{% endfor -%}
</ul>
{%- endif -%}
</li>
{%- endmacro %}
<article>
{{ content | safe }}
<ul class="elv-toc-list">
{%- for entry in navPages %}{{ renderNavListItem(entry) }}{%- endfor -%}
</ul>
</div>
</div>
{{ content | safe }}
{% include 'components/suggestion-link.html' %}
{% include 'components/suggestion-form.html' %}
</article>
</main>

View File

@ -3,12 +3,9 @@
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
/*
* Modified with an approximation of the One Light syntax highlighting theme.
*/
code[class*="language-"],
pre[class*="language-"] {
color: #494b53;
color: #ABB2BF;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
@ -29,15 +26,13 @@ pre[class*="language-"] {
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
color: #232324;
background: #dbdbdc;
background: #383e49;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
color: #232324;
background: #dbdbdc;
background: #9aa2b1;
}
@media print {
@ -55,7 +50,7 @@ pre[class*="language-"] {
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #fafafa;
background: #282c34;
}
/* Inline code */
@ -69,16 +64,16 @@ pre[class*="language-"] {
.token.prolog,
.token.doctype,
.token.cdata {
color: #505157;
color: #5C6370;
}
.token.punctuation {
color: #526fff;
color: #abb2bf;
}
.token.selector,
.token.tag {
color: none;
color: #e06c75;
}
.token.property,
@ -88,7 +83,7 @@ pre[class*="language-"] {
.token.symbol,
.token.attr-name,
.token.deleted {
color: #986801;
color: #d19a66;
}
.token.string,
@ -96,7 +91,7 @@ pre[class*="language-"] {
.token.attr-value,
.token.builtin,
.token.inserted {
color: #50a14f;
color: #98c379;
}
.token.operator,
@ -104,22 +99,22 @@ pre[class*="language-"] {
.token.url,
.language-css .token.string,
.style .token.string {
color: #526fff;
color: #56b6c2;
}
.token.atrule,
.token.keyword {
color: #e45649;
color: #e06c75;
}
.token.function {
color: #4078f2;
color: #61afef;
}
.token.regex,
.token.important,
.token.variable {
color: #e45649;
color: #c678dd;
}
.token.important,

View File

@ -1,9 +1,5 @@
---
title: How to add preparation and cleanup steps to backups
eleventyNavigation:
key: Add preparation and cleanup steps
parent: How-to guides
order: 8
---
## Preparation and cleanup hooks
@ -33,33 +29,9 @@ configuration file, right before the `create` action. `after_backup` hooks run
afterwards, but not if an error occurs in a previous hook or in the backups
themselves.
There are additional hooks that run before/after other actions as well. For
instance, `before_prune` runs before a `prune` action, while `after_prune`
runs after it.
## Variable interpolation
The before and after action hooks support interpolating particular runtime
variables into the hook command. Here's an example that assumes you provide a
separate shell script:
```yaml
hooks:
after_prune:
- record-prune.sh "{configuration_filename}" "{repositories}"
```
In this example, when the hook is triggered, borgmatic interpolates runtime
values into the hook command: the borgmatic configuration filename and the
paths of all configured repositories. Here's the full set of supported
variables you can use here:
* `configuration_filename`: borgmatic configuration filename in which the
hook was defined
* `repositories`: comma-separated paths of all repositories configured in the
current borgmatic configuration file
## Global hooks
There are additional hooks for the `prune` and `check` actions as well.
`before_prune` and `after_prune` run if there are any `prune` actions, while
`before_check` and `after_check` run if there are any `check` actions.
You can also use `before_everything` and `after_everything` hooks to perform
global setup or cleanup:
@ -82,8 +54,6 @@ but only if there is a `create` action. It runs even if an error occurs during
a backup or a backup hook, but not if an error occurs during a
`before_everything` hook.
## Error hooks
borgmatic also runs `on_error` hooks if an error occurs, either when creating
a backup or running a backup hook. See the [monitoring and alerting
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
@ -103,3 +73,11 @@ with the user permissions of borgmatic itself. So to prevent potential shell
injection or privilege escalation, do not forget to set secure permissions
on borgmatic configuration files (`chmod 0600`) and scripts (`chmod 0700`)
invoked by hooks.
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)

View File

@ -1,9 +1,5 @@
---
title: How to backup to a removable drive or an intermittent server
eleventyNavigation:
key: Backup to a removable drive or server
parent: How-to guides
order: 9
---
## Occasional backups
@ -16,14 +12,9 @@ But if you run borgmatic and your hard drive isn't plugged in, or your buddy's
server is offline, then you'll get an annoying error message and the overall
borgmatic run will fail (even if individual repositories still complete).
Another variant is when the source machine is only sometimes available for
backups, e.g. a laptop where you want to skip backups when the battery falls
below a certain level.
So what if you want borgmatic to swallow the error of a missing drive
or an offline server or a low battery—and exit gracefully? That's where the
concept of "soft failure" come in.
or an offline server, and continue trucking along? That's where the concept of
"soft failure" come in.
## Soft failure command hooks
@ -83,17 +74,6 @@ hooks:
- ping -q -c 1 buddys-server.org > /dev/null || exit 75
```
Or to only run backups if the battery level is high enough:
```yaml
hooks:
before_backup:
- is_battery_percent_at_least.sh 25
```
(Writing the battery script is left as an exercise to the reader.)
## Caveats and details
There are some caveats you should be aware of with this feature.
@ -115,6 +95,13 @@ There are some caveats you should be aware of with this feature.
* The soft failure doesn't have to apply to a repository. You can even perform
a test to make sure that individual source directories are mounted and
available. Use your imagination!
* The soft failure feature also works for before/after hooks for other
actions as well. But it is not implemented for `before_everything` or
`after_everything`.
* The soft failure feature also works for `before_prune`, `after_prune`,
`before_check`, and `after_check` hooks. But it is not implemented for
`before_everything` or `after_everything`.
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)

View File

@ -1,9 +1,5 @@
---
title: How to backup your databases
eleventyNavigation:
key: Backup your databases
parent: How-to guides
order: 7
---
## Database dump hooks
@ -15,8 +11,7 @@ consistent snapshot that is more suited for backups.
Fortunately, borgmatic includes built-in support for creating database dumps
prior to running backups. For example, here is everything you need to dump and
backup a couple of local PostgreSQL databases, a MySQL/MariaDB database, and a
MongoDB database:
backup a couple of local PostgreSQL databases and a MySQL/MariaDB database:
```yaml
hooks:
@ -25,15 +20,12 @@ hooks:
- name: orders
mysql_databases:
- name: posts
mongodb_databases:
- name: messages
```
As part of each backup, borgmatic streams a database dump for each configured
database directly to Borg, so it's included in the backup without consuming
additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
dump formats, which can't stream and therefore do consume temporary disk
space.)
additional disk space. (The one exception is PostgreSQL's "directory" dump
format, which can't stream and therefore does consume temporary disk space.)
To support this, borgmatic creates temporary named pipes in `~/.borgmatic` by
default. To customize this path, set the `borgmatic_source_directory` option
@ -42,7 +34,6 @@ in the `location` section of borgmatic's configuration.
Also note that using a database hook implicitly enables both the
`read_special` and `one_file_system` configuration settings (even if they're
disabled in your configuration) to support this dump and restore streaming.
See Limitations below for more on this.
Here's a more involved example that connects to remote databases:
@ -63,14 +54,6 @@ hooks:
username: root
password: trustsome1
options: "--skip-comments"
mongodb_databases:
- name: messages
hostname: database3.example.org
port: 27018
username: dbuser
password: trustsome1
authentication_database: mongousers
options: "--ssl"
```
If you want to dump all databases on a host, use `all` for the database name:
@ -81,24 +64,11 @@ hooks:
- name: all
mysql_databases:
- name: all
mongodb_databases:
- name: all
```
Note that you may need to use a `username` of the `postgres` superuser for
this to work with PostgreSQL.
If you would like to backup databases only and not source directories, you can
specify an empty `source_directories` value (as it is a mandatory field):
```yaml
location:
source_directories: []
hooks:
mysql_databases:
- name: all
```
### Configuration backups
@ -111,7 +81,7 @@ bring back any missing configuration files in order to restore a database.
## Supported databases
As of now, borgmatic supports PostgreSQL, MySQL/MariaDB, and MongoDB databases
As of now, borgmatic supports PostgreSQL and MySQL/MariaDB databases
directly. But see below about general-purpose preparation and cleanup hooks as
a work-around with other database systems. Also, please [file a
ticket](https://torsion.org/borgmatic/#issues) for additional database systems
@ -197,12 +167,6 @@ borgmatic's own configuration file. So include your configuration file in
backups to avoid getting caught without a way to restore a database.
3. borgmatic does not currently support backing up or restoring multiple
databases that share the exact same name on different hosts.
4. Because database hooks implicitly enable the `read_special` configuration
setting to support dump and restore streaming, you'll need to ensure that any
special files are excluded from backups (named pipes, block devices,
character devices, and sockets) to prevent hanging. Try a command like
`find /your/source/path -type c,b,p,s` to find such files. Common directories
to exclude are `/dev` and `/run`, but that may not be exhaustive.
### Manual restoration
@ -210,8 +174,8 @@ to exclude are `/dev` and `/run`, but that may not be exhaustive.
If you prefer to restore a database without the help of borgmatic, first
[extract](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/) an
archive containing a database dump, and then manually restore the dump file
found within the extracted `~/.borgmatic/` path (e.g. with `pg_restore`,
`mysql`, or `mongorestore`, commands).
found within the extracted `~/.borgmatic/` path (e.g. with `pg_restore` or
`mysql` commands).
## Preparation and cleanup hooks
@ -241,13 +205,10 @@ hooks:
options: "--single-transaction --quick"
```
### borgmatic hangs during backup
See Limitations above about `read_special`. You may need to exclude certain
paths with named pipes, block devices, character devices, or sockets on which
borgmatic is hanging.
## Related documentation
Alternatively, if excluding special files is too onerous, you can create two
separate borgmatic configuration files—one for your source files and a
separate one for backing up databases. That way, the database `read_special`
option will not be active when backing up special files.
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)

View File

@ -1,28 +1,23 @@
---
title: How to deal with very large backups
eleventyNavigation:
key: Deal with very large backups
parent: How-to guides
order: 3
---
## Biggish data
Borg itself is great for efficiently de-duplicating data across successive
backup archives, even when dealing with very large repositories. But you may
find that while borgmatic's default mode of `prune`, `compact`, `create`, and
`check` works well on small repositories, it's not so great on larger ones.
That's because running the default pruning, compact, and consistency checks
take a long time on large repositories.
find that while borgmatic's default mode of "prune, create, and check" works
well on small repositories, it's not so great on larger ones. That's because
running the default pruning and consistency checks take a long time on large
repositories.
### A la carte actions
If you find yourself in this situation, you have some options. First, you can
run borgmatic's `prune`, `compact`, `create`, or `check` actions separately.
For instance, the following optional actions are available:
run borgmatic's pruning, creating, or checking actions separately. For
instance, the the following optional actions are available:
```bash
borgmatic prune
borgmatic compact
borgmatic create
borgmatic check
```
@ -33,7 +28,7 @@ borgmatic check
You can run with only one of these actions provided, or you can mix and match
any number of them in a single borgmatic run. This supports approaches like
skipping certain actions while running others. For instance, this skips
`prune` and `compact` and only runs `create` and `check`:
`prune` and only runs `create` and `check`:
```bash
borgmatic create check
@ -125,3 +120,8 @@ the following to the `~/.ssh/config` file on the client:
This should make the client keep the connection alive while validating
backups.
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)

View File

@ -1,26 +1,22 @@
---
title: How to develop on borgmatic
eleventyNavigation:
key: Develop on borgmatic
parent: How-to guides
order: 12
---
## Source code
To get set up to hack on borgmatic, first clone master via HTTPS or SSH:
```bash
git clone https://projects.torsion.org/borgmatic-collective/borgmatic.git
git clone https://projects.torsion.org/witten/borgmatic.git
```
Or:
```bash
git clone ssh://git@projects.torsion.org:3022/borgmatic-collective/borgmatic.git
git clone ssh://git@projects.torsion.org:3022/witten/borgmatic.git
```
Then, install borgmatic
"[editable](https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs)"
"[editable](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs)"
so that you can run borgmatic commands while you're hacking on them to
make sure your changes work.
@ -66,6 +62,8 @@ following:
tox -e black
```
Note that Black requires at minimum Python 3.6.
And if you get a complaint from the
[isort](https://github.com/timothycrosley/isort) Python import orderer, you
can ask isort to order your imports for you:
@ -116,7 +114,7 @@ See the Black, Flake8, and isort documentation for more information.
Each pull request triggers a continuous integration build which runs the test
suite. You can view these builds on
[build.torsion.org](https://build.torsion.org/borgmatic-collective/borgmatic), and they're
[build.torsion.org](https://build.torsion.org/witten/borgmatic), and they're
also linked from the commits list on each pull request.
## Documentation development
@ -141,3 +139,7 @@ http://localhost:8080 to view the documentation with your changes.
To close the documentation server, ctrl-C the script. Note that it does not
currently auto-reload, so you'll need to stop it and re-run it for any
additional documentation changes to take effect.
## Related documentation
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)

View File

@ -1,9 +1,5 @@
---
title: How to extract a backup
eleventyNavigation:
key: Extract a backup
parent: How-to guides
order: 6
---
## Extract
@ -136,3 +132,11 @@ When you're all done exploring your files, unmount your mount point. No
```bash
borgmatic umount --mount-point /mnt
```
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)

View File

@ -1,5 +0,0 @@
---
eleventyNavigation:
key: How-to guides
permalink: false
---

View File

@ -1,9 +1,5 @@
---
title: How to inspect your backups
eleventyNavigation:
key: Inspect your backups
parent: How-to guides
order: 4
---
## Backup progress
@ -102,3 +98,11 @@ Note that if you use the `--log-file` flag, you are responsible for rotating
the log file so it doesn't grow too large, for example with
[logrotate](https://wiki.archlinux.org/index.php/Logrotate). Also, there is a
`--log-file-verbosity` flag to customize the log file's log level.
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)

View File

@ -1,44 +0,0 @@
---
title: How to make backups redundant
eleventyNavigation:
key: Make backups redundant
parent: How-to guides
order: 2
---
## Multiple repositories
If you really care about your data, you probably want more than one backup of
it. borgmatic supports this in its configuration by specifying multiple backup
repositories. Here's an example:
```yaml
location:
# List of source directories to backup.
source_directories:
- /home
- /etc
# Paths of local or remote repositories to backup to.
repositories:
- 1234@usw-s001.rsync.net:backups.borg
- k8pDxu32@k8pDxu32.repo.borgbase.com:repo
- /var/lib/backups/local.borg
```
When you run borgmatic with this configuration, it invokes Borg once for each
configured repository in sequence. (So, not in parallel.) That means—in each
repository—borgmatic creates a single new backup archive containing all of
your source directories.
Here's a way of visualizing what borgmatic does with the above configuration:
1. Backup `/home` and `/etc` to `1234@usw-s001.rsync.net:backups.borg`
2. Backup `/home` and `/etc` to `k8pDxu32@k8pDxu32.repo.borgbase.com:repo`
3. Backup `/home` and `/etc` to `/var/lib/backups/local.borg`
This gives you redundancy of your data across repositories and even
potentially across providers.
See [Borg repository URLs
documentation](https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls)
for more information on how to specify local and remote repository paths.

View File

@ -1,9 +1,5 @@
---
title: How to make per-application backups
eleventyNavigation:
key: Make per-application backups
parent: How-to guides
order: 1
---
## Multiple backup configurations
@ -133,47 +129,27 @@ Whatever the reason, you can override borgmatic configuration options at the
command-line via the `--override` flag. Here's an example:
```bash
borgmatic create --override location.remote_path=/usr/local/bin/borg1
borgmatic create --override location.remote_path=borg1
```
What this does is load your configuration files, and for each one, disregard
the configured value for the `remote_path` option in the `location` section,
and use the value of `/usr/local/bin/borg1` instead.
and use the value of `borg1` instead.
You can even override multiple values at once. For instance:
```bash
borgmatic create --override section.option1=value1 section.option2=value2
```
This will accomplish the same thing:
```bash
borgmatic create --override section.option1=value1 --override section.option2=value2
```
Note that each value is parsed as an actual YAML string, so you can even set
Note that the value is parsed as an actual YAML string, so you can even set
list values by using brackets. For instance:
```bash
borgmatic create --override location.repositories=[test1.borg,test2.borg]
```
Or even a single list element:
```bash
borgmatic create --override location.repositories=[/root/test1.borg]
```
There is not currently a way to override a single element of a list without
replacing the whole list.
Note that if you override an option of the list type (like
`location.repositories`), you do need to use the `[ ]` list syntax. See the
[configuration
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
which options are list types. (YAML list values look like `- this` with an
indentation and a leading dash.)
Be sure to quote your overrides if they contain spaces or other characters
that your shell may interpret.
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)

View File

@ -1,9 +1,5 @@
---
title: How to monitor your backups
eleventyNavigation:
key: Monitor your backups
parent: How-to guides
order: 5
---
## Monitoring and alerting
@ -14,68 +10,47 @@ and alerting comes in.
There are several different ways you can monitor your backups and find out
whether they're succeeding. Which of these you choose to do is up to you and
your particular infrastructure.
your particular infrastructure:
### Job runner alerts
The easiest place to start is with failure alerts from the [scheduled job
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot)
(cron, systemd, etc.) that's running borgmatic. But note that if the job
doesn't even get scheduled (e.g. due to the job runner not running), you
probably won't get an alert at all! Still, this is a decent first line of
defense, especially when combined with some of the other approaches below.
### Commands run on error
The `on_error` hook allows you to run an arbitrary command or script when
borgmatic itself encounters an error running your backups. So for instance,
you can run a script to send yourself a text message alert. But note that if
borgmatic doesn't actually run, this alert won't fire. See [error
1. **Job runner alerts**: The easiest place to start is with failure alerts
from the [scheduled job
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot) (cron,
systemd, etc.) that's running borgmatic. But note that if the job doesn't even
get scheduled (e.g. due to the job runner not running), you probably won't get
an alert at all! Still, this is a decent first line of defense, especially
when combined with some of the other approaches below.
2. **borgmatic error hooks**: The `on_error` hook allows you to run an arbitrary
command or script when borgmatic itself encounters an error running your
backups. So for instance, you can run a script to send yourself a text message
alert. But note that if borgmatic doesn't actually run, this alert won't fire.
See [error
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
below for how to configure this.
### Third-party monitoring services
borgmatic integrates with monitoring services like
[Healthchecks](https://healthchecks.io/), [Cronitor](https://cronitor.io),
[Cronhub](https://cronhub.io), and [PagerDuty](https://www.pagerduty.com/) and
pings these services whenever borgmatic runs. That way, you'll receive an
alert when something goes wrong or (for certain hooks) the service doesn't
hear from borgmatic for a configured interval. See [Healthchecks
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook),
[Cronitor
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook),
[Cronhub
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook),
and [PagerDuty
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
4. **borgmatic monitoring hooks**: This feature integrates with monitoring
services like [Healthchecks](https://healthchecks.io/),
[Cronitor](https://cronitor.io), [Cronhub](https://cronhub.io), and
[PagerDuty](https://www.pagerduty.com/) and pings these services whenever
borgmatic runs. That way, you'll receive an alert when something goes wrong or
(for certain hooks) the service doesn't hear from borgmatic for a configured
interval. See [Healthchecks
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook), [Cronitor
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook), [Cronhub
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook), and
[PagerDuty hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
below for how to configure this.
While these services offer different features, you probably only need to use
one of them at most.
### Third-party monitoring software
You can use traditional monitoring software to consume borgmatic JSON output
and track when the last successful backup occurred. See [scripting
3. **Third-party monitoring software**: You can use traditional monitoring
software to consume borgmatic JSON output and track when the last
successful backup occurred. See [scripting
borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#scripting-borgmatic)
and [related
software](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#related-software)
below for how to configure this.
### Borg hosting providers
Most [Borg hosting
5. **Borg hosting providers**: Most [Borg hosting
providers](https://torsion.org/borgmatic/#hosting-providers) include
monitoring and alerting as part of their offering. This gives you a dashboard
to check on all of your backups, and can alert you if the service doesn't hear
from borgmatic for a configured interval.
### Consistency checks
While not strictly part of monitoring, if you really want confidence that your
backups are not only running but are restorable as well, you can configure
particular [consistency
6. **borgmatic consistency checks**: While not strictly part of monitoring, if you
really want confidence that your backups are not only running but are
restorable as well, you can configure particular [consistency
checks](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration)
or even script full [extract
tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
@ -83,10 +58,10 @@ tests](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/).
## Error hooks
When an error occurs during a `prune`, `compact`, `create`, or `check` 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:
When an error occurs during a `prune`, `create`, or `check` 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:
@ -104,9 +79,10 @@ hooks:
- send-text-message.sh "{configuration_filename}" "{repository}"
```
In this example, when the error occurs, borgmatic interpolates runtime values
into the hook command: the borgmatic configuration filename, and the path of
the repository. Here's the full set of supported variables you can use here:
In this example, when the error occurs, borgmatic interpolates a few runtime
values into the hook command: the borgmatic configuration filename, and the
path of the repository. Here's the full set of supported variables you can use
here:
* `configuration_filename`: borgmatic configuration filename in which the
error occurred
@ -116,9 +92,9 @@ the repository. Here's the full set of supported variables you can use here:
* `output`: output of the command that failed (may be blank if an error
occurred without running a command)
Note that borgmatic runs the `on_error` hooks only for `prune`, `compact`,
`create`, or `check` actions or hooks in which an error occurs, and not other
actions. borgmatic does not run `on_error` hooks if an error occurs within a
Note that borgmatic runs the `on_error` hooks only for `prune`, `create`, or
`check` actions or hooks in which an error occurs, and not other actions.
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/),
@ -140,10 +116,10 @@ hooks:
```
With this hook in place, borgmatic pings your Healthchecks project when a
backup begins, ends, or errors. Specifically, after the <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 it has started if any of
the `prune`, `compact`, `create`, or `check` actions are run.
the `prune`, `create`, or `check` actions are run.
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
the success after the `after_backup` hooks run, and includes borgmatic logs in
@ -151,10 +127,10 @@ 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 any action or hook, 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 when a `prune`,
`compact`, `create`, or `check` action is run.
If an error occurs during any action, 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 when a `prune`, `create`, or
`check` action is run.
You can customize the verbosity of the logs that are sent to Healthchecks with
borgmatic's `--monitoring-verbosity` flag. The `--files` and `--stats` flags
@ -180,13 +156,13 @@ hooks:
```
With this hook in place, borgmatic pings your Cronitor monitor when a backup
begins, ends, or errors. Specifically, after the <a
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 Cronitor know that it has started if any of the
`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
complete successfully, borgmatic notifies Cronitor of the success after the
`after_backup` hooks run. And if an error occurs during any action or hook,
borgmatic notifies Cronitor after the `on_error` hooks run.
`prune`, `create`, or `check` actions are run. Then, if the actions complete
successfully, borgmatic notifies Cronitor of the success after the
`after_backup` hooks run. And if an error occurs during any action, borgmatic
notifies Cronitor after the `on_error` hooks run.
You can configure Cronitor to notify you by a [variety of
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups fail
@ -208,13 +184,13 @@ hooks:
```
With this hook in place, borgmatic pings your Cronhub monitor when a backup
begins, ends, or errors. Specifically, after the <a
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 Cronhub know that it has started if any of the
`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
complete successfully, borgmatic notifies Cronhub of the success after the
`after_backup` hooks run. And if an error occurs during any action or hook,
borgmatic notifies Cronhub after the `on_error` hooks run.
`prune`, `create`, or `check` actions are run. Then, if the actions complete
successfully, borgmatic notifies Cronhub of the success after the
`after_backup` hooks run. And if an error occurs during any action, borgmatic
notifies Cronhub after the `on_error` hooks run.
Note that even though you configure borgmatic with the "start" variant of the
ping URL, borgmatic substitutes the correct state into the URL when pinging
@ -251,9 +227,9 @@ hooks:
With this hook in place, borgmatic creates a PagerDuty event for your service
whenever backups fail. Specifically, if an error occurs during a `create`,
`prune`, `compact`, or `check` action, borgmatic sends an event to PagerDuty
before the `on_error` hooks run. Note that borgmatic does not contact
PagerDuty when a backup starts or ends without error.
`prune`, or `check` action, borgmatic sends an event to PagerDuty after the
`on_error` hooks run. Note that borgmatic does not contact PagerDuty when a
backup starts or ends without error.
You can configure PagerDuty to notify you by a [variety of
mechanisms](https://support.pagerduty.com/docs/notifications) when backups
@ -274,11 +250,6 @@ suppressed so as not to interfere with the captured JSON. Also note that JSON
output only shows up at the console, and not in syslog.
## Related software
* [Borgmacator GNOME AppIndicator](https://github.com/N-Coder/borgmacator/)
### Successful backups
`borgmatic list` includes support for a `--successful` flag that only lists
@ -312,3 +283,12 @@ automated scripts. Here's an example:
```bash
borgmatic info --archive latest
```
## Related documentation
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
* [Extract a backup](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)

View File

@ -1,94 +0,0 @@
---
title: How to run arbitrary Borg commands
eleventyNavigation:
key: Run arbitrary Borg commands
parent: How-to guides
order: 10
---
## Running Borg with borgmatic
Borg has several commands (and options) that borgmatic does not currently
support. Sometimes though, as a borgmatic user, you may find yourself wanting
to take advantage of these off-the-beaten-path Borg features. You could of
course drop down to running Borg directly. But then you'd give up all the
niceties of your borgmatic configuration. You could file a [borgmatic
ticket](https://torsion.org/borgmatic/#issues) or even a [pull
request](https://torsion.org/borgmatic/#contributing) to add the feature. But
what if you need it *now*?
That's where borgmatic's support for running "arbitrary" Borg commands comes
in. Running Borg commands with borgmatic takes advantage of the following, all
based on your borgmatic configuration files or command-line arguments:
* configured repositories (automatically runs your Borg command once for each
one)
* local and remote Borg binary paths
* SSH settings and Borg environment variables
* lock wait settings
* verbosity
### borg action
The way you run Borg with borgmatic is via the `borg` action. Here's a simple
example:
```bash
borgmatic borg break-lock
```
(No `borg` action in borgmatic? Time to upgrade!)
This runs Borg's `break-lock` command once on each configured borgmatic
repository. Notice how the repository isn't present in the specified Borg
options, as that part is provided by borgmatic.
You can also specify Borg options for relevant commands:
```bash
borgmatic borg list --progress
```
This runs Borg's `list` command once on each configured borgmatic
repository. However, the native `borgmatic list` action should be preferred
for most use.
What if you only want to run Borg on a single configured borgmatic repository
when you've got several configured? Not a problem.
```bash
borgmatic borg --repository repo.borg break-lock
```
And what about a single archive?
```bash
borgmatic borg --archive your-archive-name list
```
### Limitations
borgmatic's `borg` action is not without limitations:
* The Borg command you want to run (`create`, `list`, etc.) *must* come first
after the `borg` action. If you have any other Borg options to specify,
provide them after. For instance, `borgmatic borg list --progress` will work,
but `borgmatic borg --progress list` will not.
* borgmatic supplies the repository/archive name to Borg for you (based on
your borgmatic configuration or the `borgmatic borg --repository`/`--archive`
arguments), so do not specify the repository/archive otherwise.
* The `borg` action will not currently work for any Borg commands like `borg
serve` that do not accept a repository/archive name.
* Do not specify any global borgmatic arguments to the right of the `borg`
action. (They will be passed to Borg instead of borgmatic.) If you have
global borgmatic arguments, specify them *before* the `borg` action.
* Unlike other borgmatic actions, you cannot combine the `borg` action with
other borgmatic actions. This is to prevent ambiguity in commands like
`borgmatic borg list`, in which `list` is both a valid Borg command and a
borgmatic action. In this case, only the Borg command is run.
* Unlike normal borgmatic actions that support JSON, the `borg` action will
not disable certain borgmatic logs to avoid interfering with JSON output.
In general, this `borgmatic borg` feature should be considered an escape
valve—a feature of second resort. In the long run, it's preferable to wrap
Borg commands with borgmatic actions that can support them fully.

View File

@ -1,9 +1,5 @@
---
title: How to set up backups
eleventyNavigation:
key: Set up backups
parent: How-to guides
order: 0
title: How to set up backups with borgmatic
---
## Installation
@ -12,10 +8,9 @@ these instructions install and run borgmatic as root. If you don't need to
backup such files, then you are welcome to install and run borgmatic as a
non-root user.
First, manually [install
First, [install
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
version 1.1. borgmatic does not install Borg automatically so as to avoid
conflicts with existing Borg installations.
version 1.1.
Then, download and install borgmatic as a [user site
installation](https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site)
@ -28,7 +23,7 @@ sudo pip3 install --user --upgrade borgmatic
This installs borgmatic and its commands at the `/root/.local/bin` path.
Your pip binary may have a different name than "pip3". Make sure you're using
Python 3.7+, as borgmatic does not support older versions of Python.
Python 3, as borgmatic does not support Python 2.
The next step is to ensure that borgmatic's commands available are on your
system `PATH`, so that you can run borgmatic:
@ -68,8 +63,9 @@ sudo pip3 install --upgrade borgmatic
The main downside of a global install is that borgmatic is less cleanly
separated from the rest of your Python software, and there's the theoretical
possibility of library conflicts. But if you're okay with that, for instance
on a relatively dedicated system, then a global install can work out fine.
possibility of libary conflicts. But if you're okay with that, for instance
on a relatively dedicated system, then a global install can work out just
fine.
### Other ways to install
@ -77,7 +73,7 @@ on a relatively dedicated system, then a global install can work out fine.
Besides the approaches described above, there are several other options for
installing borgmatic:
* [Docker image with scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/) (+ Docker Compose files)
* [Docker image with scheduled backups](https://hub.docker.com/r/b3vis/borgmatic/)
* [Docker base image](https://hub.docker.com/r/monachus/borgmatic/)
* [Debian](https://tracker.debian.org/pkg/borgmatic)
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
@ -87,26 +83,22 @@ installing borgmatic:
* [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=borgmatic)
* [OpenBSD](http://ports.su/sysutils/borgmatic)
* [openSUSE](https://software.opensuse.org/package/borgmatic)
* [Ansible role](https://github.com/borgbase/ansible-role-borgbackup)
* [stand-alone binary](https://github.com/cmarquardt/borgmatic-binary)
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
## Hosting providers
Need somewhere to store your encrypted off-site backups? The following hosting
providers include specific support for Borg/borgmatic—and fund borgmatic
development and hosting when you use these links to sign up. (These are
referral links, but without any tracking scripts or cookies.)
Need somewhere to store your encrypted offsite backups? The following hosting
providers include specific support for Borg/borgmatic. Using these links and
services helps support borgmatic development and hosting. (These are referral
links, but without any tracking scripts or cookies.)
<ul>
<li class="referral"><a href="https://www.rsync.net/cgi-bin/borg.cgi?campaign=borg&adgroup=borgmatic">rsync.net</a>: Cloud Storage provider with full support for borg and any other SSH/SFTP tool</li>
<li class="referral"><a href="https://www.borgbase.com/?utm_source=borgmatic">BorgBase</a>: Borg hosting service with support for monitoring, 2FA, and append-only repos</li>
</ul>
Additionally, [rsync.net](https://www.rsync.net/products/borg.html) and
[Hetzner](https://www.hetzner.com/storage/storage-box) have compatible storage
offerings, but do not currently fund borgmatic development or hosting.
## Configuration
After you install borgmatic, generate a sample configuration file:
@ -142,7 +134,8 @@ configuration](https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-your
### Encryption
If you encrypt your Borg repository with a passphrase or a key file, you'll
Note that if you plan to run borgmatic on a schedule with cron, and you
encrypt your Borg repository with a passphrase instead of a key file, you'll
either need to set the borgmatic `encryption_passphrase` configuration
variable or set the `BORG_PASSPHRASE` environment variable. See the
[repository encryption
@ -156,13 +149,6 @@ FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-e
for more info.
### Redundancy
If you'd like to configure your backups to go to multiple different
repositories, see the documentation on how to [make backups
redundant](https://torsion.org/borgmatic/docs/how-to/make-backups-redundant/).
### Validation
If you'd like to validate that your borgmatic configuration is valid, the
@ -227,8 +213,8 @@ sudo borgmatic --verbosity 1 --files
borgmatic. So try leaving it out, or upgrade borgmatic!)
By default, this will also prune any old backups as per the configured
retention policy, compact segments to free up space (with Borg 1.2+), and
check backups for consistency problems due to things like file damage.
retention policy, and check backups for consistency problems due to things
like file damage.
The verbosity flag makes borgmatic show the steps it's performing. And the
files flag lists each file that's new or changed since the last backup.
@ -248,7 +234,7 @@ that, you can configure a separate job runner to invoke it periodically.
### cron
If you're using cron, download the [sample cron
file](https://projects.torsion.org/borgmatic-collective/borgmatic/src/master/sample/cron/borgmatic).
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/cron/borgmatic).
Then, from the directory where you downloaded it:
```bash
@ -256,26 +242,15 @@ sudo mv borgmatic /etc/cron.d/borgmatic
sudo chmod +x /etc/cron.d/borgmatic
```
If borgmatic is installed at a different location than
`/root/.local/bin/borgmatic`, edit the cron file with the correct path. You
can also modify the cron file if you'd like to run borgmatic more or less
frequently.
You can modify the cron file if you'd like to run borgmatic more or less frequently.
### systemd
If you're using systemd instead of cron to run jobs, you can still configure
borgmatic to run automatically.
(If you installed borgmatic from [Other ways to
install](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
you may already have borgmatic systemd service and timer files. If so, you may
be able to skip some of the steps below.)
First, download the [sample systemd service
file](https://projects.torsion.org/borgmatic-collective/borgmatic/raw/branch/master/sample/systemd/borgmatic.service)
If you're using systemd instead of cron to run jobs, download the [sample
systemd service
file](https://projects.torsion.org/witten/borgmatic/raw/branch/master/sample/systemd/borgmatic.service)
and the [sample systemd timer
file](https://projects.torsion.org/borgmatic-collective/borgmatic/raw/branch/master/sample/systemd/borgmatic.timer).
file](https://projects.torsion.org/witten/borgmatic/raw/branch/master/sample/systemd/borgmatic.timer).
Then, from the directory where you downloaded them:
```bash
@ -283,10 +258,6 @@ sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/
sudo systemctl enable --now borgmatic.timer
```
Review the security settings in the service file and update them as needed.
If `ProtectSystem=strict` is enabled and local repositories are used, then
the repository path must be added to the `ReadWritePaths` list.
Feel free to modify the timer file based on how frequently you'd like
borgmatic to run.
@ -295,8 +266,7 @@ borgmatic to run.
If you run borgmatic in macOS with launchd, you may encounter permissions
issues when reading files to backup. If that happens to you, you may be
interested in an [unofficial work-around for Full Disk
Access](https://projects.torsion.org/borgmatic-collective/borgmatic/issues/293).
Access](https://projects.torsion.org/witten/borgmatic/issues/293).
## Colored output
@ -306,7 +276,6 @@ non-interactive terminal is detected (like a cron job), or when you use the
setting the environment variable `PY_COLORS=False`, or setting the `color`
option to `false` in the `output` section of configuration.
## Troubleshooting
### "found character that cannot start any token" error
@ -334,3 +303,14 @@ YAML library. If so, not to worry. borgmatic should install and function
correctly even without the C YAML library. And borgmatic won't be any faster
with the C library present, so you don't need to go out of your way to install
it.
## Related documentation
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)

View File

@ -1,9 +1,5 @@
---
title: How to upgrade borgmatic
eleventyNavigation:
key: Upgrade borgmatic
parent: How-to guides
order: 11
---
## Upgrading
@ -115,3 +111,8 @@ sudo pip3 install --user borgmatic
That's it! borgmatic will continue using your /etc/borgmatic configuration
files.
## Related documentation
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)

View File

@ -1,9 +1,5 @@
---
title: Command-line reference
eleventyNavigation:
key: Command-line reference
parent: Reference guides
order: 1
title: borgmatic command-line reference
---
## borgmatic options

View File

@ -1,9 +1,5 @@
---
title: Configuration reference
eleventyNavigation:
key: Configuration reference
parent: Reference guides
order: 0
title: borgmatic configuration reference
---
## Configuration file

View File

@ -1,5 +0,0 @@
---
eleventyNavigation:
key: Reference guides
permalink: false
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/static/rsyncnet.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -1,3 +1,3 @@
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --verbosity -1 --syslog-verbosity 1
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --syslog-verbosity 1

View File

@ -2,50 +2,11 @@
Description=borgmatic backup
Wants=network-online.target
After=network-online.target
# Prevent borgmatic from running unless the machine is plugged into power. Remove this line if you
# want to allow borgmatic to run anytime.
ConditionACPower=true
[Service]
Type=oneshot
# Security settings for systemd running as root, optional but recommended to improve security. You
# can disable individual settings if they cause problems for your use case. For more details, see
# the systemd manual: https://www.freedesktop.org/software/systemd/man/systemd.exec.html
LockPersonality=true
# Certain borgmatic features like Healthchecks integration need MemoryDenyWriteExecute to be off.
# But you can try setting it to "yes" for improved security if you don't use those features.
MemoryDenyWriteExecute=no
NoNewPrivileges=yes
PrivateDevices=yes
PrivateTmp=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# To restrict write access further, change "ProtectSystem" to "strict" and uncomment
# "ReadWritePaths", "ReadOnlyPaths", "ProtectHome", and "BindPaths". Then add any local repository
# paths to the list of "ReadWritePaths" and local backup source paths to "ReadOnlyPaths". This
# leaves most of the filesystem read-only to borgmatic.
ProtectSystem=full
# ReadWritePaths=-/mnt/my_backup_drive
# ReadOnlyPaths=-/var/lib/my_backup_source
# This will mount a tmpfs on top of /root and pass through needed paths
# ProtectHome=tmpfs
# BindPaths=-/root/.cache/borg -/root/.cache/borg -/root/.borgmatic
# May interfere with running external programs within borgmatic hooks.
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
# Lower CPU and I/O priority.
Nice=19
CPUSchedulingPolicy=batch
@ -61,4 +22,4 @@ LogRateLimitIntervalSec=0
# Delay start to prevent backups running during boot. Note that systemd-inhibit requires dbus and
# dbus-user-session to be installed.
ExecStartPre=sleep 1m
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --verbosity -1 --syslog-verbosity 1
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --syslog-verbosity 1

View File

@ -38,7 +38,7 @@ for sub_command in prune create check list info; do
| grep -v '^--json$' \
| grep -v '^--keep-last$' \
| grep -v '^--list$' \
| grep -v '^--bsdflags$' \
| grep -v '^--nobsdflags$' \
| grep -v '^--pattern$' \
| grep -v '^--progress$' \
| grep -v '^--stats$' \
@ -54,7 +54,7 @@ for sub_command in prune create check list info; do
| grep -v '^--format' \
| grep -v '^--glob-archives' \
| grep -v '^--last' \
| grep -v '^--format' \
| grep -v '^--list-format' \
| grep -v '^--patterns-from' \
| grep -v '^--prefix' \
| grep -v '^--short' \

View File

@ -15,12 +15,6 @@ if [[ ! -f NEWS ]]; then
fi
version=$(head --lines=1 NEWS)
if [[ $version =~ .*dev* ]]; then
echo "Refusing to release a dev version: $version"
exit 1
fi
git tag $version
git push origin $version
git push github $version
@ -31,15 +25,14 @@ python3 setup.py bdist_wheel
python3 setup.py sdist
gpg --detach-sign --armor dist/borgmatic-*.tar.gz
gpg --detach-sign --armor dist/borgmatic-*-py3-none-any.whl
twine upload -r pypi --username __token__ dist/borgmatic-*.tar.gz dist/borgmatic-*.tar.gz.asc
twine upload -r pypi --username __token__ dist/borgmatic-*-py3-none-any.whl dist/borgmatic-*-py3-none-any.whl.asc
twine upload -r pypi dist/borgmatic-*.tar.gz dist/borgmatic-*.tar.gz.asc
twine upload -r pypi dist/borgmatic-*-py3-none-any.whl dist/borgmatic-*-py3-none-any.whl.asc
# Set release changelogs on projects.torsion.org and GitHub.
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
escaped_release_changelog="$(echo "$release_changelog" | sed -z 's/\n/\\n/g' | sed -z 's/\"/\\"/g')"
curl --silent --request POST \
"https://projects.torsion.org/api/v1/repos/borgmatic-collective/borgmatic/releases" \
--header "Authorization: token $projects_token" \
"https://projects.torsion.org/api/v1/repos/witten/borgmatic/releases?access_token=$projects_token" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "{\"body\": \"$escaped_release_changelog\", \"draft\": false, \"name\": \"borgmatic $version\", \"prerelease\": false, \"tag_name\": \"$version\"}"

View File

@ -10,12 +10,9 @@
set -e
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
py3-ruamel.yaml py3-ruamel.yaml.clib
# If certain dependencies of black are available in this version of Alpine, install them.
apk add --no-cache py3-typed-ast py3-regex || true
python3 -m pip install --no-cache --upgrade pip==22.0.3 setuptools==60.8.1
pip3 install tox==3.24.5
python -m pip install --upgrade pip==20.0.2
pip install tox==3.14.3
export COVERAGE_FILE=/tmp/.coverage
tox --workdir /tmp/.tox --sitepackages
tox --workdir /tmp/.tox --sitepackages -e end-to-end
tox --workdir /tmp/.tox
apk add --no-cache borgbackup postgresql-client mariadb-client
tox --workdir /tmp/.tox -e end-to-end

View File

@ -1,5 +1,5 @@
[metadata]
description_file=README.md
description-file=README.md
[tool:pytest]
testpaths = tests

View File

@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.5.25.dev0'
VERSION = '1.5.5'
setup(
@ -30,12 +30,11 @@ setup(
},
obsoletes=['atticmatic'],
install_requires=(
'jsonschema',
'pykwalify>=1.6.0,<14.06',
'requests',
'ruamel.yaml>0.15.0,<0.18.0',
'ruamel.yaml>0.15.0,<0.17.0',
'setuptools',
'colorama>=0.4.1,<0.5',
),
include_package_data=True,
python_requires='>3.7.0',
)

View File

@ -1,23 +1,25 @@
appdirs==1.4.4; python_version >= '3.8'
attrs==20.3.0; python_version >= '3.8'
black==19.10b0; python_version >= '3.8'
click==7.1.2; python_version >= '3.8'
colorama==0.4.4
coverage==5.3
flake8==4.0.1
appdirs==1.4.3
atomicwrites==1.3.0
attrs==19.3.0
black==19.3b0; python_version >= '3.6'
click==7.0
colorama==0.4.1
coverage==4.5.4
docopt==0.6.2
flake8==3.7.9
flexmock==0.10.4
isort==5.9.1
isort==4.3.21
mccabe==0.6.1
pluggy==0.13.1
pathspec==0.8.1; python_version >= '3.8'
py==1.10.0
pycodestyle==2.8.0
pyflakes==2.4.0
jsonschema==3.2.0
pytest==6.2.5
pytest-cov==3.0.0
regex; python_version >= '3.8'
requests==2.25.0
ruamel.yaml>0.15.0,<0.18.0
toml==0.10.2; python_version >= '3.8'
typed-ast; python_version >= '3.8'
more-itertools==7.2.0
pluggy==0.13.0
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
pykwalify==1.7.0
pytest==5.2.2
pytest-cov==2.8.1
python-dateutil==2.8.0
PyYAML==5.1.2
requests==2.22.0
ruamel.yaml>0.15.0,<0.17.0
toml==0.10.0

View File

@ -1,7 +1,7 @@
version: '3'
services:
postgresql:
image: postgres:13.1-alpine
image: postgres:12.2-alpine
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: test
@ -10,13 +10,8 @@ services:
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test
mongodb:
image: mongo:5.0.5
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: test
tests:
image: alpine:3.13
image: python:3.8-alpine3.11
volumes:
- "../..:/app:ro"
tmpfs:

View File

@ -19,8 +19,6 @@ def generate_configuration(config_path, repository_path):
open(config_path)
.read()
.replace('user@backupserver:sourcehostname.borg', repository_path)
.replace('- user@backupserver:{fqdn}', '')
.replace('- /home/user/path with spaces', '')
.replace('- /home', '- {}'.format(config_path))
.replace('- /etc', '')
.replace('- /var/log/syslog*', '')

View File

@ -47,22 +47,13 @@ hooks:
hostname: mysql
username: root
password: test
mongodb_databases:
- name: test
hostname: mongodb
username: root
password: test
authentication_database: admin
- name: all
hostname: mongodb
username: root
password: test
'''.format(
config_path, repository_path, borgmatic_source_directory, postgresql_dump_format
)
with open(config_path, 'w') as config_file:
config_file.write(config)
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
def test_database_dump_and_restore():
@ -78,15 +69,15 @@ def test_database_dump_and_restore():
write_configuration(config_path, repository_path, borgmatic_source_directory)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
'borgmatic -v 2 --config {} init --encryption repokey'.format(config_path).split(' ')
)
# Run borgmatic to generate a backup archive including a database dump.
subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
subprocess.check_call('borgmatic create --config {} -v 2'.format(config_path).split(' '))
# Get the created archive name.
output = subprocess.check_output(
['borgmatic', '--config', config_path, 'list', '--json']
'borgmatic --config {} list --json'.format(config_path).split(' ')
).decode(sys.stdout.encoding)
parsed_output = json.loads(output)
@ -96,7 +87,9 @@ def test_database_dump_and_restore():
# Restore the database from the archive.
subprocess.check_call(
['borgmatic', '--config', config_path, 'restore', '--archive', archive_name]
'borgmatic --config {} restore --archive {}'.format(config_path, archive_name).split(
' '
)
)
finally:
os.chdir(original_working_directory)
@ -121,15 +114,15 @@ def test_database_dump_and_restore_with_directory_format():
)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
'borgmatic -v 2 --config {} init --encryption repokey'.format(config_path).split(' ')
)
# Run borgmatic to generate a backup archive including a database dump.
subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
subprocess.check_call('borgmatic create --config {} -v 2'.format(config_path).split(' '))
# Restore the database from the archive.
subprocess.check_call(
['borgmatic', '--config', config_path, 'restore', '--archive', 'latest']
'borgmatic --config {} restore --archive latest'.format(config_path).split(' ')
)
finally:
os.chdir(original_working_directory)
@ -149,7 +142,7 @@ def test_database_dump_with_error_causes_borgmatic_to_exit():
write_configuration(config_path, repository_path, borgmatic_source_directory)
subprocess.check_call(
['borgmatic', '-v', '2', '--config', config_path, 'init', '--encryption', 'repokey']
'borgmatic -v 2 --config {} init --encryption repokey'.format(config_path).split(' ')
)
# Run borgmatic with a config override such that the database dump fails.

View File

@ -1,17 +0,0 @@
from borgmatic.borg import feature as module
def test_available_true_for_new_enough_borg_version():
assert module.available(module.Feature.COMPACT, '1.3.7')
def test_available_true_for_borg_version_introducing_feature():
assert module.available(module.Feature.COMPACT, '1.2.0a2')
def test_available_true_for_borg_stable_version_introducing_feature():
assert module.available(module.Feature.COMPACT, '1.2.0')
def test_available_false_for_too_old_borg_version():
assert not module.available(module.Feature.COMPACT, '1.1.5')

View File

@ -71,35 +71,6 @@ def test_parse_arguments_with_log_file_verbosity_overrides_default():
assert global_arguments.log_file_verbosity == -1
def test_parse_arguments_with_single_override_parses():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--override', 'foo.bar=baz')
global_arguments = arguments['global']
assert global_arguments.overrides == ['foo.bar=baz']
def test_parse_arguments_with_multiple_overrides_parses():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--override', 'foo.bar=baz', 'foo.quux=7')
global_arguments = arguments['global']
assert global_arguments.overrides == ['foo.bar=baz', 'foo.quux=7']
def test_parse_arguments_with_multiple_overrides_and_flags_parses():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments(
'--override', 'foo.bar=baz', '--override', 'foo.quux=7', 'this.that=8'
)
global_arguments = arguments['global']
assert global_arguments.overrides == ['foo.bar=baz', 'foo.quux=7', 'this.that=8']
def test_parse_arguments_with_list_json_overrides_default():
arguments = module.parse_arguments('list', '--json')
@ -163,24 +134,6 @@ def test_parse_arguments_with_help_and_action_shows_action_help(capsys):
assert 'create arguments:' in captured.out
def test_parse_arguments_with_action_before_global_options_parses_options():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('prune', '--verbosity', '2')
assert 'prune' in arguments
assert arguments['global'].verbosity == 2
def test_parse_arguments_with_global_options_before_action_parses_options():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--verbosity', '2', 'prune')
assert 'prune' in arguments
assert arguments['global'].verbosity == 2
def test_parse_arguments_with_prune_action_leaves_other_actions_disabled():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])

View File

@ -87,8 +87,8 @@ location:
assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip()
def testrender_configuration_converts_configuration_to_yaml_string():
yaml_string = module.render_configuration({'foo': 'bar'})
def test_render_configuration_converts_configuration_to_yaml_string():
yaml_string = module._render_configuration({'foo': 'bar'})
assert yaml_string == 'foo: bar\n'
@ -122,44 +122,38 @@ def test_write_configuration_with_already_existing_directory_does_not_raise():
def test_add_comments_to_configuration_sequence_of_strings_does_not_raise():
config = module.yaml.comments.CommentedSeq(['foo', 'bar'])
schema = {'type': 'array', 'items': {'type': 'string'}}
schema = {'seq': [{'type': 'str'}]}
module.add_comments_to_configuration_sequence(config, schema)
def test_add_comments_to_configuration_sequence_of_maps_does_not_raise():
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
schema = {
'type': 'array',
'items': {'type': 'object', 'properties': {'foo': {'description': 'yo'}}},
}
schema = {'seq': [{'map': {'foo': {'desc': 'yo'}}}]}
module.add_comments_to_configuration_sequence(config, schema)
def test_add_comments_to_configuration_sequence_of_maps_without_description_does_not_raise():
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
schema = {'type': 'array', 'items': {'type': 'object', 'properties': {'foo': {}}}}
schema = {'seq': [{'map': {'foo': {}}}]}
module.add_comments_to_configuration_sequence(config, schema)
def test_add_comments_to_configuration_object_does_not_raise():
def test_add_comments_to_configuration_map_does_not_raise():
# Ensure that it can deal with fields both in the schema and missing from the schema.
config = module.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
schema = {
'type': 'object',
'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
}
schema = {'map': {'foo': {'desc': 'Foo'}, 'bar': {'desc': 'Bar'}}}
module.add_comments_to_configuration_object(config, schema)
module.add_comments_to_configuration_map(config, schema)
def test_add_comments_to_configuration_object_with_skip_first_does_not_raise():
def test_add_comments_to_configuration_map_with_skip_first_does_not_raise():
config = module.yaml.comments.CommentedMap([('foo', 33)])
schema = {'type': 'object', 'properties': {'foo': {'description': 'Foo'}}}
schema = {'map': {'foo': {'desc': 'Foo'}}}
module.add_comments_to_configuration_object(config, schema, skip_first=True)
module.add_comments_to_configuration_map(config, schema, skip_first=True)
def test_remove_commented_out_sentinel_keeps_other_comments():
@ -200,7 +194,7 @@ def test_generate_sample_configuration_does_not_raise():
flexmock(module.yaml).should_receive('round_trip_load')
flexmock(module).should_receive('_schema_to_sample_configuration')
flexmock(module).should_receive('merge_source_configuration_into_destination')
flexmock(module).should_receive('render_configuration')
flexmock(module).should_receive('_render_configuration')
flexmock(module).should_receive('_comment_out_optional_configuration')
flexmock(module).should_receive('write_configuration')
@ -214,7 +208,7 @@ def test_generate_sample_configuration_with_source_filename_does_not_raise():
flexmock(module.load).should_receive('load_configuration')
flexmock(module).should_receive('_schema_to_sample_configuration')
flexmock(module).should_receive('merge_source_configuration_into_destination')
flexmock(module).should_receive('render_configuration')
flexmock(module).should_receive('_render_configuration')
flexmock(module).should_receive('_comment_out_optional_configuration')
flexmock(module).should_receive('write_configuration')

View File

@ -1,8 +0,0 @@
MAXIMUM_LINE_LENGTH = 80
def test_schema_line_length_stays_under_limit():
schema_file = open('borgmatic/config/schema.yaml')
for line in schema_file.readlines():
assert len(line.rstrip('\n')) <= MAXIMUM_LINE_LENGTH

View File

@ -1,24 +0,0 @@
import logging
from flexmock import flexmock
from borgmatic.hooks import healthchecks as module
def test_destroy_monitor_removes_healthchecks_handler():
logger = logging.getLogger()
original_handlers = list(logger.handlers)
logger.addHandler(module.Forgetful_buffering_handler(byte_capacity=100, log_level=1))
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock())
assert logger.handlers == original_handlers
def test_destroy_monitor_without_healthchecks_handler_does_not_raise():
logger = logging.getLogger()
original_handlers = list(logger.handlers)
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock())
assert logger.handlers == original_handlers

View File

@ -1,6 +1,5 @@
import logging
import subprocess
import sys
import pytest
from flexmock import flexmock
@ -89,21 +88,13 @@ def test_log_outputs_skips_error_output_in_exception_for_process_with_none_stdou
def test_log_outputs_kills_other_processes_when_one_errors():
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)
flexmock(module).should_receive('command_for_process').and_return('grep')
process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
flexmock(module).should_receive('exit_code_indicates_error').with_args(
process, None, 'borg'
).and_return(False)
flexmock(module).should_receive('exit_code_indicates_error').with_args(
process, 2, 'borg'
).and_return(True)
other_process = subprocess.Popen(
['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
['watch', 'true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
flexmock(module).should_receive('exit_code_indicates_error').with_args(
other_process, None, 'borg'
).and_return(False)
flexmock(module).should_receive('output_buffer_for_process').with_args(process, ()).and_return(
process.stdout
)
@ -124,87 +115,13 @@ def test_log_outputs_kills_other_processes_when_one_errors():
assert error.value.output
def test_log_outputs_vents_other_processes_when_one_exits():
'''
Execute a command to generate a longish random string and pipe it into another command that
exits quickly. The test is basically to ensure we don't hang forever waiting for the exited
process to read the pipe, and that the string-generating process eventually gets vented and
exits.
'''
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('command_for_process').and_return('grep')
process = subprocess.Popen(
[
sys.executable,
'-c',
"import random, string; print(''.join(random.choice(string.ascii_letters) for _ in range(40000)))",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
other_process = subprocess.Popen(
['true'], stdin=process.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
flexmock(module).should_receive('output_buffer_for_process').with_args(
process, (process.stdout,)
).and_return(process.stderr)
flexmock(module).should_receive('output_buffer_for_process').with_args(
other_process, (process.stdout,)
).and_return(other_process.stdout)
flexmock(process.stdout).should_call('readline').at_least().once()
module.log_outputs(
(process, other_process),
exclude_stdouts=(process.stdout,),
output_log_level=logging.INFO,
borg_local_path='borg',
)
def test_log_outputs_does_not_error_when_one_process_exits():
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('command_for_process').and_return('grep')
process = subprocess.Popen(
[
sys.executable,
'-c',
"import random, string; print(''.join(random.choice(string.ascii_letters) for _ in range(40000)))",
],
stdout=None, # Specifically test the case of a process without stdout captured.
stderr=None,
)
other_process = subprocess.Popen(
['true'], stdin=process.stdout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
flexmock(module).should_receive('output_buffer_for_process').with_args(
process, (process.stdout,)
).and_return(process.stderr)
flexmock(module).should_receive('output_buffer_for_process').with_args(
other_process, (process.stdout,)
).and_return(other_process.stdout)
module.log_outputs(
(process, other_process),
exclude_stdouts=(process.stdout,),
output_log_level=logging.INFO,
borg_local_path='borg',
)
def test_log_outputs_truncates_long_error_output():
flexmock(module).ERROR_OUTPUT_MAX_LINE_COUNT = 0
flexmock(module.logger).should_receive('log')
flexmock(module).should_receive('exit_code_indicates_error').and_return(True)
flexmock(module).should_receive('command_for_process').and_return('grep')
process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
flexmock(module).should_receive('exit_code_indicates_error').with_args(
process, None, 'borg'
).and_return(False)
flexmock(module).should_receive('exit_code_indicates_error').with_args(
process, 2, 'borg'
).and_return(True)
flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
with pytest.raises(subprocess.CalledProcessError) as error:
@ -226,16 +143,3 @@ def test_log_outputs_with_no_output_logs_nothing():
module.log_outputs(
(process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg'
)
def test_log_outputs_with_unfinished_process_re_polls():
flexmock(module.logger).should_receive('log').never()
flexmock(module).should_receive('exit_code_indicates_error').and_return(False)
process = subprocess.Popen(['true'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
flexmock(process).should_receive('poll').and_return(None).and_return(0).twice()
flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
module.log_outputs(
(process,), exclude_stdouts=(), output_log_level=logging.INFO, borg_local_path='borg'
)

View File

@ -1,123 +0,0 @@
import logging
from flexmock import flexmock
from borgmatic.borg import borg as module
from ..test_verbosity import insert_logging_mock
def test_run_arbitrary_borg_calls_borg_with_parameters():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg'
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
)
def test_run_arbitrary_borg_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--info'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
insert_logging_mock(logging.INFO)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
)
def test_run_arbitrary_borg_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--debug', '--show-rc'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
insert_logging_mock(logging.DEBUG)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'],
)
def test_run_arbitrary_borg_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--lock-wait', '5'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config=storage_config, options=['break-lock'],
)
def test_run_arbitrary_borg_with_archive_calls_borg_with_archive_parameter():
storage_config = {}
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo::archive'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config=storage_config, options=['break-lock'], archive='archive',
)
def test_run_arbitrary_borg_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('execute_command').with_args(
('borg1', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg1'
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'], local_path='borg1',
)
def test_run_arbitrary_borg_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo', '--remote-path', 'borg1'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['break-lock'], remote_path='borg1',
)
def test_run_arbitrary_borg_passes_borg_specific_parameters_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo', '--progress'),
output_log_level=logging.WARNING,
borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['list', '--progress'],
)
def test_run_arbitrary_borg_omits_dash_dash_in_parameters_passed_to_borg():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'break-lock', 'repo'), output_log_level=logging.WARNING, borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=['--', 'break-lock'],
)
def test_run_arbitrary_borg_without_borg_specific_parameters_does_not_raise():
flexmock(module).should_receive('execute_command').with_args(
('borg',), output_log_level=logging.WARNING, borg_local_path='borg',
)
module.run_arbitrary_borg(
repository='repo', storage_config={}, options=[],
)

View File

@ -1,110 +0,0 @@
import logging
from flexmock import flexmock
from borgmatic.borg import compact as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(compact_command, output_log_level):
flexmock(module).should_receive('execute_command').with_args(
compact_command, output_log_level=output_log_level, borg_local_path=compact_command[0]
).once()
COMPACT_COMMAND = ('borg', 'compact')
def test_compact_segments_calls_borg_with_parameters():
insert_execute_command_mock(COMPACT_COMMAND + ('repo',), logging.INFO)
module.compact_segments(dry_run=False, repository='repo', storage_config={})
def test_compact_segments_with_log_info_calls_borg_with_info_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--info', 'repo'), logging.INFO)
insert_logging_mock(logging.INFO)
module.compact_segments(repository='repo', storage_config={}, dry_run=False)
def test_compact_segments_with_log_debug_calls_borg_with_debug_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--debug', '--show-rc', 'repo'), logging.INFO)
insert_logging_mock(logging.DEBUG)
module.compact_segments(repository='repo', storage_config={}, dry_run=False)
def test_compact_segments_with_dry_run_skips_borg_call():
flexmock(module).should_receive('execute_command').never()
module.compact_segments(repository='repo', storage_config={}, dry_run=True)
def test_compact_segments_with_local_path_calls_borg_via_local_path():
insert_execute_command_mock(('borg1',) + COMPACT_COMMAND[1:] + ('repo',), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, local_path='borg1',
)
def test_compact_segments_with_remote_path_calls_borg_with_remote_path_parameters():
insert_execute_command_mock(COMPACT_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, remote_path='borg1',
)
def test_compact_segments_with_progress_calls_borg_with_progress_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--progress', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, progress=True,
)
def test_compact_segments_with_cleanup_commits_calls_borg_with_cleanup_commits_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--cleanup-commits', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, cleanup_commits=True,
)
def test_compact_segments_with_threshold_calls_borg_with_threshold_parameter():
insert_execute_command_mock(COMPACT_COMMAND + ('--threshold', '20', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config={}, threshold=20,
)
def test_compact_segments_with_umask_calls_borg_with_umask_parameters():
storage_config = {'umask': '077'}
insert_execute_command_mock(COMPACT_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
)
def test_compact_segments_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
insert_execute_command_mock(COMPACT_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False, repository='repo', storage_config=storage_config,
)
def test_compact_segments_with_extra_borg_options_calls_borg_with_extra_options():
insert_execute_command_mock(COMPACT_COMMAND + ('--extra', '--options', 'repo'), logging.INFO)
module.compact_segments(
dry_run=False,
repository='repo',
storage_config={'extra_borg_options': {'compact': '--extra --options'}},
)

File diff suppressed because it is too large Load Diff

View File

@ -60,25 +60,3 @@ def test_initialize_with_relocated_repo_access_should_override_default():
assert os.environ.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
finally:
os.environ = orig_environ
def test_initialize_prefers_configuration_option_over_borg_environment_variable():
orig_environ = os.environ
try:
os.environ = {'BORG_SSH': 'mosh'}
module.initialize({'ssh_command': 'ssh -C'})
assert os.environ.get('BORG_RSH') == 'ssh -C'
finally:
os.environ = orig_environ
def test_initialize_passes_through_existing_borg_environment_variable():
orig_environ = os.environ
try:
os.environ = {'BORG_PASSPHRASE': 'pass'}
module.initialize({'ssh_command': 'ssh -C'})
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
finally:
os.environ = orig_environ

View File

@ -1,226 +0,0 @@
import logging
from flexmock import flexmock
from borgmatic.borg import export_tar as module
from ..test_verbosity import insert_logging_mock
def insert_execute_command_mock(
command, output_log_level=logging.INFO, borg_local_path='borg', capture=True
):
flexmock(module).should_receive('execute_command').with_args(
command,
output_file=None if capture else module.DO_NOT_CAPTURE,
output_log_level=output_log_level,
borg_local_path=borg_local_path,
).once()
def test_export_tar_archive_calls_borg_with_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', 'repo::archive', 'test.tar', 'path1', 'path2')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=['path1', 'path2'],
destination_path='test.tar',
storage_config={},
)
def test_export_tar_archive_calls_borg_with_local_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg1', 'export-tar', 'repo::archive', 'test.tar'), borg_local_path='borg1'
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
local_path='borg1',
)
def test_export_tar_archive_calls_borg_with_remote_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--remote-path', 'borg1', 'repo::archive', 'test.tar')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
remote_path='borg1',
)
def test_export_tar_archive_calls_borg_with_umask_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--umask', '0770', 'repo::archive', 'test.tar')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={'umask': '0770'},
)
def test_export_tar_archive_calls_borg_with_lock_wait_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--lock-wait', '5', 'repo::archive', 'test.tar')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={'lock_wait': '5'},
)
def test_export_tar_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'export-tar', '--info', 'repo::archive', 'test.tar'))
insert_logging_mock(logging.INFO)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
)
def test_export_tar_archive_with_log_debug_calls_borg_with_debug_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--debug', '--show-rc', 'repo::archive', 'test.tar')
)
insert_logging_mock(logging.DEBUG)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
)
def test_export_tar_archive_calls_borg_with_dry_run_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module).should_receive('execute_command').never()
module.export_tar_archive(
dry_run=True,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
)
def test_export_tar_archive_calls_borg_with_tar_filter_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--tar-filter', 'bzip2', 'repo::archive', 'test.tar')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
tar_filter='bzip2',
)
def test_export_tar_archive_calls_borg_with_list_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--list', 'repo::archive', 'test.tar'),
output_log_level=logging.WARNING,
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
files=True,
)
def test_export_tar_archive_calls_borg_with_strip_components_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(
('borg', 'export-tar', '--strip-components', '5', 'repo::archive', 'test.tar')
)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
strip_components=5,
)
def test_export_tar_archive_skips_abspath_for_remote_repository_parameter():
flexmock(module.os.path).should_receive('abspath').never()
insert_execute_command_mock(('borg', 'export-tar', 'server:repo::archive', 'test.tar'))
module.export_tar_archive(
dry_run=False,
repository='server:repo',
archive='archive',
paths=None,
destination_path='test.tar',
storage_config={},
)
def test_export_tar_archive_calls_borg_with_stdout_destination_path():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'export-tar', 'repo::archive', '-'), capture=False)
module.export_tar_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
destination_path='-',
storage_config={},
)

View File

@ -25,14 +25,12 @@ def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'
)
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -43,7 +41,6 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet
)
insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -56,7 +53,6 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param
('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2')
)
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
@ -66,7 +62,6 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path():
('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'
)
insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None, local_path='borg1')
@ -78,7 +73,6 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2')
)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None, remote_path='borg1')
@ -90,7 +84,6 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
insert_execute_command_mock(
('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2')
)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
@ -98,7 +91,6 @@ def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_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'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -107,14 +99,12 @@ def test_extract_archive_calls_borg_with_path_parameters():
paths=['path1', 'path2'],
location_config={},
storage_config={},
local_borg_version='1.2.3',
)
def test_extract_archive_calls_borg_with_remote_path_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -123,18 +113,13 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
remote_path='borg1',
)
@pytest.mark.parametrize(
'feature_available,option_flag', ((True, '--numeric-ids'), (False, '--numeric-owner'),),
)
def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available, option_flag):
def test_extract_archive_calls_borg_with_numeric_owner_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', option_flag, 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(feature_available)
insert_execute_command_mock(('borg', 'extract', '--numeric-owner', 'repo::archive'))
module.extract_archive(
dry_run=False,
@ -143,14 +128,12 @@ def test_extract_archive_calls_borg_with_numeric_ids_parameter(feature_available
paths=None,
location_config={'numeric_owner': True},
storage_config={},
local_borg_version='1.2.3',
)
def test_extract_archive_calls_borg_with_umask_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -159,14 +142,12 @@ def test_extract_archive_calls_borg_with_umask_parameters():
paths=None,
location_config={},
storage_config={'umask': '0770'},
local_borg_version='1.2.3',
)
def test_extract_archive_calls_borg_with_lock_wait_parameters():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -175,7 +156,6 @@ def test_extract_archive_calls_borg_with_lock_wait_parameters():
paths=None,
location_config={},
storage_config={'lock_wait': '5'},
local_borg_version='1.2.3',
)
@ -183,7 +163,6 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
insert_logging_mock(logging.INFO)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -192,7 +171,6 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
)
@ -202,7 +180,6 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
('borg', 'extract', '--debug', '--list', '--show-rc', 'repo::archive')
)
insert_logging_mock(logging.DEBUG)
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -211,14 +188,12 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
)
def test_extract_archive_calls_borg_with_dry_run_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=True,
@ -227,14 +202,12 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
)
def test_extract_archive_calls_borg_with_destination_path():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', 'repo::archive'), working_directory='/dest')
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -243,28 +216,10 @@ def test_extract_archive_calls_borg_with_destination_path():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
destination_path='/dest',
)
def test_extract_archive_calls_borg_with_strip_components():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
insert_execute_command_mock(('borg', 'extract', '--strip-components', '5', 'repo::archive'))
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
repository='repo',
archive='archive',
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
strip_components=5,
)
def test_extract_archive_calls_borg_with_progress_parameter():
flexmock(module.os.path).should_receive('abspath').and_return('repo')
flexmock(module).should_receive('execute_command').with_args(
@ -272,7 +227,6 @@ def test_extract_archive_calls_borg_with_progress_parameter():
output_file=module.DO_NOT_CAPTURE,
working_directory=None,
).once()
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -281,7 +235,6 @@ def test_extract_archive_calls_borg_with_progress_parameter():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
progress=True,
)
@ -297,7 +250,6 @@ def test_extract_archive_with_progress_and_extract_to_stdout_raises():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
progress=True,
extract_to_stdout=True,
)
@ -312,7 +264,6 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
working_directory=None,
run_to_completion=False,
).and_return(process).once()
flexmock(module.feature).should_receive('available').and_return(True)
assert (
module.extract_archive(
@ -322,7 +273,6 @@ def test_extract_archive_calls_borg_with_stdout_parameter_and_returns_process():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
extract_to_stdout=True,
)
== process
@ -334,7 +284,6 @@ def test_extract_archive_skips_abspath_for_remote_repository():
flexmock(module).should_receive('execute_command').with_args(
('borg', 'extract', 'server:repo::archive'), working_directory=None
).once()
flexmock(module.feature).should_receive('available').and_return(True)
module.extract_archive(
dry_run=False,
@ -343,5 +292,4 @@ def test_extract_archive_skips_abspath_for_remote_repository():
paths=None,
location_config={},
storage_config={},
local_borg_version='1.2.3',
)

View File

@ -1,49 +0,0 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.borg import version as module
from ..test_verbosity import insert_logging_mock
VERSION = '1.2.3'
def insert_execute_command_mock(command, borg_local_path='borg', version_output=f'borg {VERSION}'):
flexmock(module).should_receive('execute_command').with_args(
command, output_log_level=None, borg_local_path=borg_local_path
).once().and_return(version_output)
def test_local_borg_version_calls_borg_with_required_parameters():
insert_execute_command_mock(('borg', '--version'))
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_log_info_calls_borg_with_info_parameter():
insert_execute_command_mock(('borg', '--version', '--info'))
insert_logging_mock(logging.INFO)
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_log_debug_calls_borg_with_debug_parameters():
insert_execute_command_mock(('borg', '--version', '--debug', '--show-rc'))
insert_logging_mock(logging.DEBUG)
assert module.local_borg_version() == VERSION
def test_local_borg_version_with_local_borg_path_calls_borg_with_it():
insert_execute_command_mock(('borg1', '--version'), borg_local_path='borg1')
assert module.local_borg_version('borg1') == VERSION
def test_local_borg_version_with_invalid_version_raises():
insert_execute_command_mock(('borg', '--version'), version_output='wtf')
with pytest.raises(ValueError):
module.local_borg_version()

View File

@ -5,140 +5,146 @@ from borgmatic.commands import arguments as module
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
action_namespace = flexmock(foo=True)
subparsers = {
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, ['action'])),
'other': flexmock(),
}
arguments, remaining_arguments = module.parse_subparser_arguments(
('--foo', 'true', 'action'), subparsers
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
assert arguments == {'action': action_namespace}
assert remaining_arguments == []
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
action_namespace = flexmock(foo=True)
subparsers = {
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, ['action'])),
'other': flexmock(),
}
arguments, remaining_arguments = module.parse_subparser_arguments(
('action', '--foo', 'true'), subparsers
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace}
assert remaining_arguments == []
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
action_namespace = flexmock(foo=True)
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, ['action']))
subparsers = {
'action': action_subparser,
'-a': action_subparser,
'other': flexmock(),
'-o': flexmock(),
}
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
subparsers = flexmock(
choices={
'action': action_subparser,
'-a': action_subparser,
'other': flexmock(),
'-o': flexmock(),
}
)
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
arguments, remaining_arguments = module.parse_subparser_arguments(
('-a', '--foo', 'true'), subparsers
)
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace}
assert remaining_arguments == []
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
action_namespace = flexmock(foo=True)
other_namespace = flexmock(bar=3)
subparsers = {
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['action', '--bar', '3'])
),
'other': flexmock(parse_known_args=lambda arguments: (other_namespace, [])),
}
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--bar', '3'])
),
'other': flexmock(parse_known_args=lambda arguments: (other_namespace, [])),
}
)
arguments, remaining_arguments = module.parse_subparser_arguments(
arguments = module.parse_subparser_arguments(
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
)
assert arguments == {'action': action_namespace, 'other': other_namespace}
assert remaining_arguments == []
def test_parse_subparser_arguments_applies_default_subparsers():
prune_namespace = flexmock()
compact_namespace = flexmock()
create_namespace = flexmock(progress=True)
check_namespace = flexmock()
subparsers = {
'prune': flexmock(
parse_known_args=lambda arguments: (prune_namespace, ['prune', '--progress'])
),
'compact': flexmock(parse_known_args=lambda arguments: (compact_namespace, [])),
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
'other': flexmock(),
}
subparsers = flexmock(
choices={
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
'create': flexmock(parse_known_args=lambda arguments: (create_namespace, [])),
'check': flexmock(parse_known_args=lambda arguments: (check_namespace, [])),
'other': flexmock(),
}
)
arguments, remaining_arguments = module.parse_subparser_arguments(('--progress'), subparsers)
arguments = module.parse_subparser_arguments(('--progress'), subparsers)
assert arguments == {
'prune': prune_namespace,
'compact': compact_namespace,
'create': create_namespace,
'check': check_namespace,
}
assert remaining_arguments == []
def test_parse_subparser_arguments_passes_through_unknown_arguments_before_subparser_name():
def test_parse_global_arguments_with_help_does_not_apply_default_subparsers():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
subparsers = {
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['action', '--verbosity', 'lots'])
),
'other': flexmock(),
}
arguments, remaining_arguments = module.parse_subparser_arguments(
('--verbosity', 'lots', 'action'), subparsers
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
assert arguments == {'action': action_namespace}
assert remaining_arguments == ['--verbosity', 'lots']
def test_parse_subparser_arguments_passes_through_unknown_arguments_after_subparser_name():
action_namespace = flexmock()
subparsers = {
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['action', '--verbosity', 'lots'])
),
'other': flexmock(),
}
arguments, remaining_arguments = module.parse_subparser_arguments(
('action', '--verbosity', 'lots'), subparsers
arguments = module.parse_global_arguments(
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
)
assert arguments == {'action': action_namespace}
assert remaining_arguments == ['--verbosity', 'lots']
assert arguments == global_namespace
def test_parse_subparser_arguments_parses_borg_options_and_skips_other_subparsers():
action_namespace = flexmock(options=[])
subparsers = {
'borg': flexmock(parse_known_args=lambda arguments: (action_namespace, ['borg', 'list'])),
'list': flexmock(),
}
def test_parse_global_arguments_consumes_global_arguments_before_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments, remaining_arguments = module.parse_subparser_arguments(('borg', 'list'), subparsers)
arguments = module.parse_global_arguments(
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
)
assert arguments == {'borg': action_namespace}
assert arguments['borg'].options == ['list']
assert remaining_arguments == []
assert arguments == global_namespace
def test_parse_global_arguments_consumes_global_arguments_after_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_global_arguments(
('action', '--verbosity', 'lots'), top_level_parser, subparsers
)
assert arguments == global_namespace

View File

@ -1,6 +1,5 @@
import logging
import subprocess
import time
from flexmock import flexmock
@ -10,7 +9,6 @@ from borgmatic.commands import borgmatic as module
def test_run_configuration_runs_actions_for_each_repository():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
expected_results[1:]
@ -23,21 +21,8 @@ def test_run_configuration_runs_actions_for_each_repository():
assert results == expected_results
def test_run_configuration_with_invalid_borg_version_errors():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
flexmock(module.command).should_receive('execute_hook').never()
flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').never()
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'prune': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_calls_hooks_for_prune_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([])
@ -47,20 +32,8 @@ def test_run_configuration_calls_hooks_for_prune_action():
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_calls_hooks_for_compact_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module).should_receive('run_actions').and_return([])
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'compact': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_executes_and_calls_hooks_for_create_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([])
@ -72,7 +45,6 @@ def test_run_configuration_executes_and_calls_hooks_for_create_action():
def test_run_configuration_calls_hooks_for_check_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').at_least().twice()
flexmock(module).should_receive('run_actions').and_return([])
@ -82,21 +54,8 @@ def test_run_configuration_calls_hooks_for_check_action():
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_calls_hooks_for_extract_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').twice()
flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').and_return([])
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'extract': flexmock()}
list(module.run_configuration('test.yaml', config, arguments))
def test_run_configuration_does_not_trigger_hooks_for_list_action():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').never()
flexmock(module.dispatch).should_receive('call_hooks').never()
flexmock(module).should_receive('run_actions').and_return([])
@ -108,11 +67,10 @@ def test_run_configuration_does_not_trigger_hooks_for_list_action():
def test_run_configuration_logs_actions_error():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module.dispatch).should_receive('call_hooks')
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False)}
@ -124,10 +82,9 @@ def test_run_configuration_logs_actions_error():
def test_run_configuration_logs_pre_hook_error():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None)
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').never()
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
@ -139,10 +96,9 @@ def test_run_configuration_logs_pre_hook_error():
def test_run_configuration_bails_for_pre_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_raise(error).and_return(None)
flexmock(module).should_receive('log_error_records').never()
flexmock(module).should_receive('make_error_log_records').never()
flexmock(module).should_receive('run_actions').never()
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
@ -154,13 +110,12 @@ def test_run_configuration_bails_for_pre_hook_soft_failure():
def test_run_configuration_logs_post_hook_error():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
OSError
).and_return(None)
flexmock(module.dispatch).should_receive('call_hooks')
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_return([])
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
@ -172,13 +127,12 @@ def test_run_configuration_logs_post_hook_error():
def test_run_configuration_bails_for_post_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
error
).and_return(None)
flexmock(module.dispatch).should_receive('call_hooks')
flexmock(module).should_receive('log_error_records').never()
flexmock(module).should_receive('make_error_log_records').never()
flexmock(module).should_receive('run_actions').and_return([])
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
@ -190,10 +144,9 @@ def test_run_configuration_bails_for_post_hook_soft_failure():
def test_run_configuration_logs_on_error_hook_error():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').and_return(
flexmock(module).should_receive('make_error_log_records').and_return(
expected_results[:1]
).and_return(expected_results[1:])
flexmock(module).should_receive('run_actions').and_raise(OSError)
@ -207,11 +160,10 @@ def test_run_configuration_logs_on_error_hook_error():
def test_run_configuration_bails_for_on_error_hook_soft_failure():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
error = subprocess.CalledProcessError(borgmatic.hooks.command.SOFT_FAIL_EXIT_CODE, 'try again')
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(error)
expected_results = [flexmock()]
flexmock(module).should_receive('log_error_records').and_return(expected_results)
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
flexmock(module).should_receive('run_actions').and_raise(OSError)
config = {'location': {'repositories': ['foo']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
@ -221,196 +173,6 @@ def test_run_configuration_bails_for_on_error_hook_soft_failure():
assert results == expected_results
def test_run_configuration_retries_soft_error():
# Run action first fails, second passes
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_return([])
flexmock(module).should_receive('log_error_records').and_return([flexmock()]).once()
config = {'location': {'repositories': ['foo']}, 'storage': {'retries': 1}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == []
def test_run_configuration_retries_hard_error():
# Run action fails twice
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()])
error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository', OSError,
).and_return(error_logs)
config = {'location': {'repositories': ['foo']}, 'storage': {'retries': 1}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
def test_run_repos_ordered():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(2)
expected_results = [flexmock(), flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository', OSError
).and_return(expected_results[:1]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', OSError
).and_return(expected_results[1:]).ordered()
config = {'location': {'repositories': ['foo', 'bar']}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == expected_results
def test_run_configuration_retries_round_robbin():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
foo_error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository', OSError
).and_return(foo_error_logs).ordered()
bar_error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', OSError
).and_return(bar_error_logs).ordered()
config = {'location': {'repositories': ['foo', 'bar']}, 'storage': {'retries': 1}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == foo_error_logs + bar_error_logs
def test_run_configuration_retries_one_passes():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
[]
).and_raise(OSError).times(4)
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return(flexmock()).ordered()
error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', OSError
).and_return(error_logs).ordered()
config = {'location': {'repositories': ['foo', 'bar']}, 'storage': {'retries': 1}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
def test_run_configuration_retry_wait():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).times(4)
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(20).and_return().ordered()
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(time).should_receive('sleep').with_args(30).and_return().ordered()
error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository', OSError
).and_return(error_logs).ordered()
config = {'location': {'repositories': ['foo']}, 'storage': {'retries': 3, 'retry_wait': 10}}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
def test_run_configuration_retries_timeout_multiple_repos():
flexmock(module.borg_environment).should_receive('initialize')
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
flexmock(module.command).should_receive('execute_hook')
flexmock(module).should_receive('run_actions').and_raise(OSError).and_raise(OSError).and_return(
[]
).and_raise(OSError).times(4)
flexmock(module).should_receive('log_error_records').with_args(
'foo: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository',
OSError,
levelno=logging.WARNING,
log_command_error_output=True,
).and_return([flexmock()]).ordered()
# Sleep before retrying foo (and passing)
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
# Sleep before retrying bar (and failing)
flexmock(time).should_receive('sleep').with_args(10).and_return().ordered()
error_logs = [flexmock()]
flexmock(module).should_receive('log_error_records').with_args(
'bar: Error running actions for repository', OSError
).and_return(error_logs).ordered()
config = {
'location': {'repositories': ['foo', 'bar']},
'storage': {'retries': 1, 'retry_wait': 10},
}
arguments = {'global': flexmock(monitoring_verbosity=1, dry_run=False), 'create': flexmock()}
results = list(module.run_configuration('test.yaml', config, arguments))
assert results == error_logs
def test_load_configurations_collects_parsed_configurations():
configuration = flexmock()
other_configuration = flexmock()
@ -424,15 +186,6 @@ def test_load_configurations_collects_parsed_configurations():
assert logs == []
def test_load_configurations_logs_warning_for_permission_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(PermissionError)
configs, logs = tuple(module.load_configurations(('test.yaml',)))
assert configs == {}
assert {log.levelno for log in logs} == {logging.WARNING}
def test_load_configurations_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
@ -450,46 +203,48 @@ def test_log_record_with_suppress_does_not_raise():
module.log_record(levelno=1, foo='bar', baz='quux', suppress_log=True)
def test_log_error_records_generates_output_logs_for_message_only():
def test_make_error_log_records_generates_output_logs_for_message_only():
flexmock(module).should_receive('log_record').replace_with(dict)
logs = tuple(module.log_error_records('Error'))
logs = tuple(module.make_error_log_records('Error'))
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
def test_log_error_records_generates_output_logs_for_called_process_error():
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.log_error_records('Error', subprocess.CalledProcessError(1, 'ls', 'error output'))
module.make_error_log_records(
'Error', subprocess.CalledProcessError(1, 'ls', 'error output')
)
)
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
assert any(log for log in logs if 'error output' in str(log))
def test_log_error_records_generates_logs_for_value_error():
def test_make_error_log_records_generates_logs_for_value_error():
flexmock(module).should_receive('log_record').replace_with(dict)
logs = tuple(module.log_error_records('Error', ValueError()))
logs = tuple(module.make_error_log_records('Error', ValueError()))
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
def test_log_error_records_generates_logs_for_os_error():
def test_make_error_log_records_generates_logs_for_os_error():
flexmock(module).should_receive('log_record').replace_with(dict)
logs = tuple(module.log_error_records('Error', OSError()))
logs = tuple(module.make_error_log_records('Error', OSError()))
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
def test_log_error_records_generates_nothing_for_other_error():
def test_make_error_log_records_generates_nothing_for_other_error():
flexmock(module).should_receive('log_record').replace_with(dict)
logs = tuple(module.log_error_records('Error', KeyError()))
logs = tuple(module.make_error_log_records('Error', KeyError()))
assert logs == ()
@ -546,7 +301,7 @@ def test_collect_configuration_run_summary_logs_extract_with_repository_error():
ValueError
)
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
arguments = {'extract': flexmock(repository='repo')}
logs = tuple(
@ -573,7 +328,7 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
ValueError
)
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
arguments = {'mount': flexmock(repository='repo')}
logs = tuple(
@ -586,7 +341,7 @@ def test_collect_configuration_run_summary_logs_mount_with_repository_error():
def test_collect_configuration_run_summary_logs_missing_configs_error():
arguments = {'global': flexmock(config_paths=[])}
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
@ -596,7 +351,7 @@ def test_collect_configuration_run_summary_logs_missing_configs_error():
def test_collect_configuration_run_summary_logs_pre_hook_error():
flexmock(module.command).should_receive('execute_hook').and_raise(ValueError)
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
logs = tuple(
@ -610,7 +365,7 @@ def test_collect_configuration_run_summary_logs_post_hook_error():
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError)
flexmock(module).should_receive('run_configuration').and_return([])
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
arguments = {'create': flexmock(), 'global': flexmock(monitoring_verbosity=1, dry_run=False)}
logs = tuple(
@ -625,7 +380,7 @@ def test_collect_configuration_run_summary_logs_for_list_with_archive_and_reposi
ValueError
)
expected_logs = (flexmock(),)
flexmock(module).should_receive('log_error_records').and_return(expected_logs)
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
arguments = {'list': flexmock(repository='repo', archive='test')}
logs = tuple(
@ -651,7 +406,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('log_error_records').and_return([])
flexmock(module).should_receive('make_error_log_records').and_return([])
arguments = {}
logs = tuple(
@ -665,7 +420,7 @@ def test_collect_configuration_run_summary_logs_run_umount_error():
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_return([])
flexmock(module.borg_umount).should_receive('unmount_archive').and_raise(OSError)
flexmock(module).should_receive('log_error_records').and_return(
flexmock(module).should_receive('make_error_log_records').and_return(
[logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
)
arguments = {'umount': flexmock(mount_point='/mnt')}
@ -681,9 +436,7 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
['baz']
)
stdout = flexmock()
stdout.should_receive('write').with_args('["foo", "bar", "baz"]').once()
flexmock(module.sys).stdout = stdout
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
arguments = {}
tuple(

View File

@ -45,7 +45,6 @@ def test_collect_config_filenames_collects_yml_file_endings():
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d').and_return(True)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yml').and_return(False)
flexmock(module.os).should_receive('access').and_return(True)
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(['foo.yml'])
@ -63,7 +62,6 @@ def test_collect_config_filenames_collects_files_from_given_directories_and_igno
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/bar').and_return(True)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/baz.yaml').and_return(False)
flexmock(module.os).should_receive('access').and_return(True)
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(
['foo.yaml', 'bar', 'baz.yaml']
@ -86,7 +84,6 @@ def test_collect_config_filenames_collects_files_from_given_directories_and_igno
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/bar.yaml~').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/baz.txt').and_return(False)
flexmock(module.os).should_receive('access').and_return(True)
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(
['foo.yaml', 'bar.yaml~', 'baz.txt']
@ -97,21 +94,6 @@ def test_collect_config_filenames_collects_files_from_given_directories_and_igno
assert config_filenames == ('/etc/borgmatic.d/foo.yaml',)
def test_collect_config_filenames_skips_permission_denied_directories():
config_paths = ('config.yaml', '/etc/borgmatic.d')
mock_path = flexmock(module.os.path)
mock_path.should_receive('exists').and_return(True)
mock_path.should_receive('isdir').with_args('config.yaml').and_return(False)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d').and_return(True)
flexmock(module.os).should_receive('access').and_return(False)
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(['config.yaml'])
config_filenames = tuple(module.collect_config_filenames(config_paths))
assert config_filenames == ('config.yaml',)
def test_collect_config_filenames_skips_etc_borgmatic_config_dot_yaml_if_it_does_not_exist():
config_paths = ('config.yaml', '/etc/borgmatic/config.yaml')
mock_path = flexmock(module.os.path)

View File

@ -12,7 +12,7 @@ Parsed_config = namedtuple('Parsed_config', ('location', 'storage', 'retention',
def test_convert_section_generates_integer_value_for_integer_type_in_schema():
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
source_section_config = OrderedDict([('check_last', '3')])
section_schema = {'type': 'object', 'properties': {'check_last': {'type': 'integer'}}}
section_schema = {'map': {'check_last': {'type': 'int'}}}
destination_config = module._convert_section(source_section_config, section_schema)
@ -21,7 +21,7 @@ def test_convert_section_generates_integer_value_for_integer_type_in_schema():
def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
flexmock(module.generate).should_receive('add_comments_to_configuration_object')
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
source_config = Parsed_config(
location=OrderedDict([('source_directories', '/home'), ('repository', 'hostname.borg')]),
storage=OrderedDict([('encryption_passphrase', 'supersecret')]),
@ -29,10 +29,7 @@ def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
consistency=OrderedDict([('checks', 'repository')]),
)
source_excludes = ['/var']
schema = {
'type': 'object',
'properties': defaultdict(lambda: {'type': 'object', 'properties': {}}),
}
schema = {'map': defaultdict(lambda: {'map': {}})}
destination_config = module.convert_legacy_parsed_config(source_config, source_excludes, schema)
@ -57,7 +54,7 @@ def test_convert_legacy_parsed_config_transforms_source_config_to_mapping():
def test_convert_legacy_parsed_config_splits_space_separated_values():
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
flexmock(module.generate).should_receive('add_comments_to_configuration_object')
flexmock(module.generate).should_receive('add_comments_to_configuration_map')
source_config = Parsed_config(
location=OrderedDict(
[('source_directories', '/home /etc'), ('repository', 'hostname.borg')]
@ -67,10 +64,7 @@ def test_convert_legacy_parsed_config_splits_space_separated_values():
consistency=OrderedDict([('checks', 'repository archives')]),
)
source_excludes = ['/var']
schema = {
'type': 'object',
'properties': defaultdict(lambda: {'type': 'object', 'properties': {}}),
}
schema = {'map': defaultdict(lambda: {'map': {}})}
destination_config = module.convert_legacy_parsed_config(source_config, source_excludes, schema)

View File

@ -8,32 +8,24 @@ from borgmatic.config import generate as module
def test_schema_to_sample_configuration_generates_config_map_with_examples():
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
flexmock(module).should_receive('add_comments_to_configuration_object')
flexmock(module).should_receive('add_comments_to_configuration_map')
schema = {
'type': 'object',
'properties': OrderedDict(
'map': OrderedDict(
[
(
'section1',
{
'type': 'object',
'properties': {'field1': OrderedDict([('example', 'Example 1')])},
},
),
('section1', {'map': {'field1': OrderedDict([('example', 'Example 1')])}}),
(
'section2',
{
'type': 'object',
'properties': OrderedDict(
'map': OrderedDict(
[
('field2', {'example': 'Example 2'}),
('field3', {'example': 'Example 3'}),
]
),
)
},
),
]
),
)
}
config = module._schema_to_sample_configuration(schema)
@ -49,7 +41,7 @@ def test_schema_to_sample_configuration_generates_config_map_with_examples():
def test_schema_to_sample_configuration_generates_config_sequence_of_strings_with_example():
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
flexmock(module).should_receive('add_comments_to_configuration_sequence')
schema = {'type': 'array', 'items': {'type': 'string'}, 'example': ['hi']}
schema = {'seq': [{'type': 'str'}], 'example': ['hi']}
config = module._schema_to_sample_configuration(schema)
@ -59,15 +51,15 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_strings_wit
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
flexmock(module).should_receive('add_comments_to_configuration_sequence')
flexmock(module).should_receive('add_comments_to_configuration_object')
flexmock(module).should_receive('add_comments_to_configuration_map')
schema = {
'type': 'array',
'items': {
'type': 'object',
'properties': OrderedDict(
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
),
},
'seq': [
{
'map': OrderedDict(
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
)
}
]
}
config = module._schema_to_sample_configuration(schema)

View File

@ -1,5 +1,4 @@
import pytest
import ruamel.yaml
from flexmock import flexmock
from borgmatic.config import override as module
@ -71,14 +70,6 @@ def test_parse_overrides_raises_on_missing_equal_sign():
module.parse_overrides(raw_overrides)
def test_parse_overrides_raises_on_invalid_override_value():
flexmock(module).should_receive('convert_value_type').and_raise(ruamel.yaml.parser.ParserError)
raw_overrides = ['section.option=[in valid]']
with pytest.raises(ValueError):
module.parse_overrides(raw_overrides)
def test_parse_overrides_allows_value_with_single_key():
flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
raw_overrides = ['option=value']

View File

@ -4,30 +4,7 @@ from flexmock import flexmock
from borgmatic.config import validate as module
def test_format_json_error_path_element_formats_array_index():
module.format_json_error_path_element(3) == '[3]'
def test_format_json_error_path_element_formats_property():
module.format_json_error_path_element('foo') == '.foo'
def test_format_json_error_formats_error_including_path():
flexmock(module).format_json_error_path_element = lambda element: '.{}'.format(element)
error = flexmock(message='oops', path=['foo', 'bar'])
assert module.format_json_error(error) == "At 'foo.bar': oops"
def test_format_json_error_formats_error_without_path():
flexmock(module).should_receive('format_json_error_path_element').never()
error = flexmock(message='oops', path=[])
assert module.format_json_error(error) == 'At the top level: oops'
def test_validation_error_string_contains_errors():
flexmock(module).format_json_error = lambda error: error.message
def test_validation_error_str_contains_error_messages_and_config_filename():
error = module.Validation_error('config.yaml', ('oops', 'uh oh'))
result = str(error)
@ -38,8 +15,6 @@ def test_validation_error_string_contains_errors():
def test_apply_logical_validation_raises_if_archive_name_format_present_without_prefix():
flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error):
module.apply_logical_validation(
'config.yaml',
@ -51,8 +26,6 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
def test_apply_logical_validation_raises_if_archive_name_format_present_without_retention_prefix():
flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error):
module.apply_logical_validation(
'config.yaml',
@ -65,8 +38,6 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
flexmock(module).format_json_error = lambda error: error.message
with pytest.raises(module.Validation_error):
module.apply_logical_validation(
'config.yaml',
@ -104,6 +75,27 @@ def test_apply_logical_validation_does_not_raise_otherwise():
module.apply_logical_validation('config.yaml', {'retention': {'keep_secondly': 1000}})
def test_remove_examples_strips_examples_from_map():
schema = {
'map': {
'foo': {'desc': 'thing1', 'example': 'bar'},
'baz': {'desc': 'thing2', 'example': 'quux'},
}
}
module.remove_examples(schema)
assert schema == {'map': {'foo': {'desc': 'thing1'}, 'baz': {'desc': 'thing2'}}}
def test_remove_examples_strips_examples_from_sequence_of_maps():
schema = {'seq': [{'map': {'foo': {'desc': 'thing', 'example': 'bar'}}, 'example': 'stuff'}]}
module.remove_examples(schema)
assert schema == {'seq': [{'map': {'foo': {'desc': 'thing'}}}]}
def test_normalize_repository_path_passes_through_remote_repository():
repository = 'example.org:test.borg'

View File

@ -58,7 +58,7 @@ def test_call_hooks_calls_each_hook_and_collects_return_values():
assert return_values == expected_return_values
def test_call_hooks_calls_skips_return_values_for_missing_hooks():
def test_call_hooks_calls_skips_return_values_for_unconfigured_hooks():
hooks = {'super_hook': flexmock()}
expected_return_values = {'super_hook': flexmock()}
flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
@ -66,13 +66,3 @@ def test_call_hooks_calls_skips_return_values_for_missing_hooks():
return_values = module.call_hooks('do_stuff', hooks, 'prefix', ('super_hook', 'other_hook'), 55)
assert return_values == expected_return_values
def test_call_hooks_calls_skips_return_values_for_null_hooks():
hooks = {'super_hook': flexmock(), 'other_hook': None}
expected_return_values = {'super_hook': flexmock()}
flexmock(module).should_receive('call_hook').and_return(expected_return_values['super_hook'])
return_values = module.call_hooks('do_stuff', hooks, 'prefix', ('super_hook', 'other_hook'), 55)
assert return_values == expected_return_values

View File

@ -47,28 +47,69 @@ def test_create_named_pipe_for_dump_does_not_raise():
module.create_named_pipe_for_dump('/path/to/pipe')
def test_remove_database_dumps_removes_dump_path():
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.shutil).should_receive('rmtree').with_args('databases/localhost').once()
def test_remove_database_dumps_removes_dump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
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')
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('isdir').and_return(False)
flexmock(module.os).should_receive('remove').with_args('databases/localhost/foo').once()
flexmock(module.os).should_receive('remove').with_args('databases/localhost/bar').once()
flexmock(module.os).should_receive('listdir').with_args('databases/localhost').and_return(
['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)
def test_remove_database_dumps_removes_dump_in_directory_format():
databases = [{'name': 'foo'}]
flexmock(module).should_receive('make_database_dump_filename').with_args(
'databases', 'foo', None
).and_return('databases/localhost/foo')
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('isdir').and_return(True)
flexmock(module.os).should_receive('remove').never()
flexmock(module.shutil).should_receive('rmtree').with_args('databases/localhost/foo').once()
flexmock(module.os).should_receive('listdir').with_args('databases/localhost').and_return([])
flexmock(module.os).should_receive('rmdir').with_args('databases/localhost').once()
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=False)
def test_remove_database_dumps_with_dry_run_skips_removal():
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.shutil).should_receive('rmtree').never()
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module.os).should_receive('rmdir').never()
flexmock(module.os).should_receive('remove').never()
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=True)
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=True)
def test_remove_database_dumps_without_dump_path_present_skips_removal():
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
def test_remove_database_dumps_without_dump_present_skips_removal():
databases = [{'name': 'foo'}]
flexmock(module).should_receive('make_database_dump_filename').with_args(
'databases', 'foo', None
).and_return('databases/localhost/foo')
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.os.path).should_receive('isdir').never()
flexmock(module.os).should_receive('remove').never()
flexmock(module.shutil).should_receive('rmtree').never()
flexmock(module.os).should_receive('rmdir').never()
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
module.remove_database_dumps('databases', databases, 'SuperDB', 'test.yaml', dry_run=False)
def test_remove_database_dumps_without_databases_does_not_raise():
module.remove_database_dumps('databases', [], 'SuperDB', 'test.yaml', dry_run=False)
def test_convert_glob_patterns_to_borg_patterns_removes_leading_slash():

View File

@ -28,9 +28,7 @@ def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reac
def test_format_buffered_logs_for_payload_flattens_log_buffer():
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
handler.buffer = ['foo\n', 'bar\n']
logger = flexmock(handlers=[handler])
logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(logger)
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
payload = module.format_buffered_logs_for_payload()
@ -41,9 +39,7 @@ def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
handler.buffer = ['foo\n', 'bar\n']
handler.forgot = True
logger = flexmock(handlers=[handler])
logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(logger)
flexmock(module.logging).should_receive('getLogger').and_return(flexmock(handlers=[handler]))
payload = module.format_buffered_logs_for_payload()
@ -51,9 +47,9 @@ def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs
def test_format_buffered_logs_for_payload_without_handler_produces_empty_payload():
logger = flexmock(handlers=[module.logging.Handler()])
logger.should_receive('removeHandler')
flexmock(module.logging).should_receive('getLogger').and_return(logger)
flexmock(module.logging).should_receive('getLogger').and_return(
flexmock(handlers=[module.logging.Handler()])
)
payload = module.format_buffered_logs_for_payload()

View File

@ -1,308 +0,0 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic.hooks import mongodb as module
def test_dump_databases_runs_mongodump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}]
processes = [flexmock(), flexmock()]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo'
).and_return('databases/localhost/bar')
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
for name, process in zip(('foo', 'bar'), processes):
flexmock(module).should_receive('execute_command').with_args(
['mongodump', '--archive', '--db', name, '>', 'databases/localhost/{}'.format(name)],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == processes
def test_dump_databases_with_dry_run_skips_mongodump():
databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo'
).and_return('databases/localhost/bar')
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').never()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=True) == []
def test_dump_databases_runs_mongodump_with_hostname_and_port():
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/database.example.org/foo'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
[
'mongodump',
'--archive',
'--host',
'database.example.org',
'--port',
'5433',
'--db',
'foo',
'>',
'databases/database.example.org/foo',
],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == [process]
def test_dump_databases_runs_mongodump_with_username_and_password():
databases = [
{
'name': 'foo',
'username': 'mongo',
'password': 'trustsome1',
'authentication_database': "admin",
}
]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
[
'mongodump',
'--archive',
'--username',
'mongo',
'--password',
'trustsome1',
'--authenticationDatabase',
'admin',
'--db',
'foo',
'>',
'databases/localhost/foo',
],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == [process]
def test_dump_databases_runs_mongodump_with_directory_format():
databases = [{'name': 'foo', 'format': 'directory'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
flexmock(module).should_receive('execute_command').with_args(
['mongodump', '--archive', 'databases/localhost/foo', '--db', 'foo'],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == [process]
def test_dump_databases_runs_mongodump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
['mongodump', '--archive', '--db', 'foo', '--stuff=such', '>', 'databases/localhost/foo'],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == [process]
def test_dump_databases_runs_mongodumpall_for_all_databases():
databases = [{'name': 'all'}]
process = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/all'
)
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
flexmock(module).should_receive('execute_command').with_args(
['mongodump', '--archive', '>', 'databases/localhost/all'],
shell=True,
run_to_completion=False,
).and_return(process).once()
assert module.dump_databases(databases, 'test.yaml', {}, dry_run=False) == [process]
def test_restore_database_dump_runs_pg_restore():
database_config = [{'name': 'foo'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '--drop', '--db', 'foo'],
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
borg_local_path='borg',
).once()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=extract_process
)
def test_restore_database_dump_errors_on_multiple_database_config():
database_config = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').never()
flexmock(module).should_receive('execute_command').never()
with pytest.raises(ValueError):
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=flexmock()
)
def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
database_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
extract_process = flexmock(stdout=flexmock())
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
'--archive',
'--drop',
'--db',
'foo',
'--host',
'database.example.org',
'--port',
'5433',
],
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
borg_local_path='borg',
).once()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=extract_process
)
def test_restore_database_dump_runs_pg_restore_with_username_and_password():
database_config = [
{
'name': 'foo',
'username': 'mongo',
'password': 'trustsome1',
'authentication_database': 'admin',
}
]
extract_process = flexmock(stdout=flexmock())
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
[
'mongorestore',
'--archive',
'--drop',
'--db',
'foo',
'--username',
'mongo',
'--password',
'trustsome1',
'--authenticationDatabase',
'admin',
],
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
borg_local_path='borg',
).once()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=extract_process
)
def test_restore_database_dump_runs_psql_for_all_database_dump():
database_config = [{'name': 'all'}]
extract_process = flexmock(stdout=flexmock())
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive'],
processes=[extract_process],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout,
borg_local_path='borg',
).once()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=extract_process
)
def test_restore_database_dump_with_dry_run_skips_restore():
database_config = [{'name': 'foo'}]
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename')
flexmock(module).should_receive('execute_command_with_processes').never()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=True, extract_process=flexmock()
)
def test_restore_database_dump_without_extract_process_restores_from_disk():
database_config = [{'name': 'foo'}]
flexmock(module).should_receive('make_dump_path')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('/dump/path')
flexmock(module).should_receive('execute_command_with_processes').with_args(
['mongorestore', '--archive', '/dump/path', '--drop', '--db', 'foo'],
processes=[],
output_log_level=logging.DEBUG,
input_file=None,
borg_local_path='borg',
).once()
module.restore_database_dump(
database_config, 'test.yaml', {}, dry_run=False, extract_process=None
)

Some files were not shown because too many files have changed in this diff Show More