Compare commits

..
131 Commits
Author SHA1 Message Date
Dan Helfman 74ac148747 Disable console color via "color" option in borgmatic configuration output section (#191). 2019-06-19 20:48:54 -07:00
Dan Helfman be7887c071 Demote log level of unhelpful info log (#194). 2019-06-19 12:01:03 -07:00
Dan Helfman da459d95b8 Bump version for release. 2019-06-17 12:16:23 -07:00
Dan Helfman b3aa6af859 Don't color syslog output (#197). 2019-06-17 11:53:08 -07:00
Dan Helfman b816af1b13 Undo purge. 2019-06-16 22:10:25 -07:00
Dan Helfman 276aeb9875 Fix tests that assert on default syslog verbosity. 2019-06-16 21:58:41 -07:00
Dan Helfman de94001508 Change default syslog verbosity to show errors only. 2019-06-16 21:57:14 -07:00
Dan Helfman 7cfab3620b Don't prune docs image after push, so watchtower can pick it up. 2019-06-16 21:52:09 -07:00
Dan Helfman 6c136ebbf1 Fix for unclear error message for invalid YAML merge include (#196). 2019-06-16 21:33:40 -07:00
Dan Helfman eaa5eb4174 Note about including config file. 2019-06-15 14:28:32 -07:00
Dan Helfman acc2a39454 Include note about debug output. 2019-06-15 14:27:53 -07:00
Dan Helfman a10c7a8496 Trying out a Gitea issue template for somewhat more structured bug reports. 2019-06-15 14:23:45 -07:00
Dan Helfman de82919e39 Skip coverage on a particular annoying-to-trigger branch in execution code. 2019-06-13 21:38:06 -07:00
Dan Helfman 1ba56d5262 Fix tests in Python 3.6. 2019-06-13 21:34:04 -07:00
Dan Helfman 1c825b5d84 Bump version for release. 2019-06-13 20:50:06 -07:00
Dan Helfman d6d66de251 Set umask used when executing hooks via "umask" option in borgmatic hooks section (#189). 2019-06-13 17:05:26 -07:00
Dan Helfman 76d79f0331 Suppress part of an obnoxious warning about disabling coverage (for end-to-end tests). 2019-06-13 14:15:08 -07:00
Dan Helfman dc43c38e29 Complete test coverage for logging branch. 2019-06-13 11:11:42 -07:00
Dan Helfman 7d7308a80d Integration tests for execute.py. 2019-06-13 10:48:21 -07:00
Dan Helfman b43ef9d76d Move test file to correspond to its code under test. 2019-06-13 10:27:00 -07:00
Dan Helfman 28cdd67743 Error hook test. 2019-06-13 10:14:16 -07:00
Dan Helfman 7f126ce127 Move hook.py file up a level. 2019-06-13 10:09:16 -07:00
Dan Helfman a6c4debf78 Additional test coverage, and upgrade test requirements. 2019-06-13 10:01:55 -07:00
Dan Helfman a74ad5475e Run all Borg commands such that they log to syslog as well as console. 2019-06-12 20:56:20 -07:00
Dan Helfman fa293828df Run hooks such that their output goes to syslog as well as console. 2019-06-12 13:09:04 -07:00
Dan Helfman f5582b1754 Move borgmatic.borg.execute module up a level for broader use. 2019-06-12 12:13:59 -07:00
Dan Helfman 1af95714c2 Collapse two execute_command() parameters into one output log level parameter. 2019-06-12 12:11:36 -07:00
Dan Helfman 0406d18cfd Log Borg --stats output as warning so it shows up at any verbosity level. 2019-06-12 11:49:35 -07:00
Dan Helfman 66e9ec9c3c A few tests for JSON flag suppressing Borg output. 2019-06-12 11:31:46 -07:00
Dan Helfman 899a7c8318 Add some wheel metadata do .gitignore. 2019-06-11 21:40:28 -07:00
Dan Helfman 7c01b69498 Details on where to view logs. 2019-06-11 21:35:43 -07:00
Dan Helfman 4f0d3bf4ed Add docs/default about systemd journald rate limiting. 2019-06-11 17:03:40 -07:00
Dan Helfman 9a5e7a3abb Successfully convert Borg output to Python logging entries. 2019-06-11 16:42:04 -07:00
Dan Helfman 02eb6c7e09 Merge branch 'master' into logging 2019-06-10 10:27:22 -07:00
Dan Helfman 418c09398c Fix incorrect compression default in schema comment. 2019-06-09 21:21:46 -07:00
Dan Helfman cdbd4c55e8 Fix 404 links harder. 2019-06-01 13:23:48 -07:00
Dan Helfman 2374410891 Fix 404 documentation links by switching to absolute links. 2019-06-01 13:02:39 -07:00
Dan Helfman d2c46e91fe Add rsync.net to hosting providers; includ random link rotation. 2019-05-29 15:35:04 -07:00
Dan Helfman 12441331e6 Fix formatting / import ordering. 2019-05-27 15:46:38 -07:00
Dan Helfman 9ceeae2de0 Add separate syslog verbosity flag. 2019-05-27 15:44:48 -07:00
Dan Helfman e0e493c2f1 Factor out configuring of logging into a common function. 2019-05-27 15:05:26 -07:00
Dan Helfman 0f05f7ad93 Log to syslog in addition to existing standard out logging (#53). 2019-05-26 16:34:46 -07:00
Dan Helfman 9bc1b71017 Clarify description in setup.py. 2019-05-26 13:36:53 -07:00
Dan Helfman b3776871b5 Rewrite the borgmatic overview a bit to clarify its place in the world. 2019-05-26 13:35:51 -07:00
Dan Helfman 308cb31bf9 Remove some of the link rewriting to hopefully fix broken docs links. 2019-05-25 21:55:28 -07:00
Dan Helfman e1f4643215 In README, use absolute links to properly rendered documentation (to cut down on confusion). 2019-05-25 21:48:05 -07:00
Dan Helfman bc4fb322b5 Move documenation build step last in CI. Refactor docs build scripts. 2019-05-21 21:33:25 -07:00
Dan Helfman 2c4f192e43 Attempt to build documentation image in CI. 2019-05-21 21:13:35 -07:00
Dan Helfman fb7a6dccaa Link to docs on skipping pruning entirely. 2019-05-21 12:32:19 -07:00
Dan HelfmanandGitea 2826b7bd7c Add files for building documentation into a Docker image for web serving. 2019-05-21 03:16:32 +00:00
Dan Helfman 932848f6c1 Merge branch 'master' into docs-image. 2019-05-20 19:23:12 -07:00
Dan HelfmanandGitea 9255940c6b Upgrade Drone build file format from 0.8 to 1.x. 2019-05-21 02:21:46 +00:00
Dan Helfman 3eadd16856 Add build server upgrade to NEWS. 2019-05-20 19:20:05 -07:00
Dan Helfman 61f46c5ad5 Try without explicit "sh". 2019-05-20 19:06:13 -07:00
Dan Helfman aad47d1741 bash -> sh. 2019-05-20 18:19:42 -07:00
Dan Helfman 079dd3fe4c Another try. 2019-05-20 18:18:30 -07:00
Dan Helfman d47f1bff4d Try to run script. 2019-05-20 18:17:25 -07:00
Dan Helfman 53967f6324 Trigger build. 2019-05-20 17:48:39 -07:00
Dan Helfman f5a70dc2a5 Drone + jsonnet apparently requires an extension, so switching back to plain YAML. 2019-05-20 17:30:07 -07:00
Dan Helfman 31ae1013d7 Add missing close curly. 2019-05-20 17:18:30 -07:00
Dan Helfman 071945e558 Re-order. 2019-05-20 17:15:01 -07:00
Dan Helfman 5c4d6a6e83 Upgrade Drone build file format from 0.8 to 1.x. 2019-05-20 17:05:29 -07:00
Dan Helfman 9c9be65b2b Add files for building documentation into a Docker image for web serving. 2019-05-20 11:41:39 -07:00
Dan HelfmanandGitea c164684703 Allow to only run unit tests with Tox. 2019-05-19 22:07:15 +00:00
Dan Helfman 842c9001ba Auto-join #borgmatic from IRC web chat. 2019-05-19 15:04:01 -07:00
Dan Helfman 481e47076e Add #borgmatic Freenode IRC channel to documentation. 2019-05-19 15:01:03 -07:00
Luke Murphy 917a0dd0a0 Pass posargs to pytest in main testenv 2019-05-19 23:53:43 +02:00
Luke Murphy 358aed7c31 Allow to only run unit tests with Tox 2019-05-19 23:53:42 +02:00
Dan HelfmanandGitea 9893834e85 Pass positional arguments to Tox environments commands. 2019-05-19 21:28:38 +00:00
Dan Helfman 32cf3225c5 Update NEWS. 2019-05-19 14:17:00 -07:00
Dan HelfmanandGitea 2bfd7518c5 Look for .yml configuration file extension in addition to .yaml (#178). 2019-05-19 21:16:25 +00:00
Dan Helfman 4ba56684d1 Update NEWS with remove Python cache files before each Tox run. 2019-05-19 10:07:28 -07:00
Dan HelfmanandGitea 0b1e38e5f6 Remove Python cache files before each Tox run. 2019-05-19 17:07:11 +00:00
Luke Murphy 7974219389 Make sure to pass posargs for Tox testenvs 2019-05-19 13:11:22 +02:00
Luke Murphy 8424e443a9 Also read .yml ending configuration files
Closes https://projects.torsion.org/witten/borgmatic/issues/178.
2019-05-19 13:04:42 +02:00
Luke Murphy 85251cf5d4 Ensure to remove cache files for Tox runs 2019-05-19 12:46:32 +02:00
Dan Helfman 8f882ea3ea Switch to more standard "utm_source" for hosting provider link. 2019-05-18 21:33:39 -07:00
Dan Helfman 7a2bcc96bb Add Borg/borgmatic hosting providers to documentation. 2019-05-18 20:59:50 -07:00
Dan Helfman 8b41e58e1f Mention isort import ordering in documentation. 2019-05-16 12:06:55 -07:00
Dan Helfman 9417359da3 Fix for regression with missing verbose log entries (#177). 2019-05-16 10:50:19 -07:00
Dan Helfman 1cf0e1bd84 Support for various Borg directory environment variables (#153). 2019-05-16 10:34:52 -07:00
Dan Helfman 223f803e87 Fix formatting. 2019-05-14 13:09:36 -07:00
Dan Helfman 6cb901d083 Bump version for release. 2019-05-14 13:07:49 -07:00
Dan HelfmanandGitea 096be14230 Run tests for all installed versions of Python (#166). 2019-05-14 20:06:08 +00:00
Dan Helfman bb8b1e58e8 PR feedback: Consistency. 2019-05-14 12:19:56 -07:00
Dan Helfman 06261d8c86 Merge branch 'master' into tox-skip-missing-interpreters 2019-05-14 12:18:30 -07:00
Dan HelfmanandGitea 869cccf884 Upgrade pip to a particular version during local test and CI. 2019-05-14 19:17:30 +00:00
Dan Helfman 0defaf9cb5 Run tests for all installed versions of Python (#166). 2019-05-14 12:09:07 -07:00
Dan Helfman 60b1f9921d Don't use pip wrapper script in CI. 2019-05-14 12:01:40 -07:00
Dan Helfman f61bc91b0f Merge branch 'master' into upgrade-pip 2019-05-14 10:31:03 -07:00
Dan Helfman ed2c6053de Upgrade pip to a particular version during local test and CI. 2019-05-14 10:28:04 -07:00
Dan Helfman 2cffa8deaa Add missing ticket number to NEWS item. 2019-05-14 10:07:46 -07:00
Dan Helfman f0581271f6 Automatically sort Python imports in code. 2019-05-14 10:02:41 -07:00
Dan HelfmanandGitea 99522234ea Automatically sort Python imports in code. 2019-05-14 17:02:37 +00:00
Dan Helfman 67f2862fb1 Change paths to reflect new pip install --user documentation. 2019-05-14 10:00:50 -07:00
Luke Murphy 1c0dc3f904 Run isort over the source 2019-05-14 18:59:19 +02:00
Luke Murphy b94dbff216 Add isort configuration
Closes https://projects.torsion.org/witten/borgmatic/issues/169.
2019-05-14 18:59:19 +02:00
Dan Helfman 7388c723cd Mention tox.ini refactoring in NEWS. 2019-05-14 09:45:39 -07:00
Dan HelfmanandGitea 128be3c17d Factor out build/test configuration from tox.ini file. 2019-05-14 16:45:24 +00:00
Luke Murphy 4c30c94258 Add workaround for editable failure
See https://projects.torsion.org/witten/borgmatic/pulls/165#issuecomment-1416.
2019-05-14 13:17:15 +02:00
Luke Murphy 20b8b45aeb Remove all configuration from Tox file
This puts tool configuration in their familiar and standard
locations and simplifies the Tox configuration to just laying
out the environments and factors.

This also allows users who do not want to deal with overhead of Tox (for
whatever reasons ...) to run pytest/black/etc. and have the same
behaviour.
2019-05-14 13:17:15 +02:00
Dan Helfman 2dd899f287 Linkify build status badge. 2019-05-13 22:33:28 -07:00
Dan Helfman a13cc0ab17 More tests for colored logging. 2019-05-13 21:10:26 -07:00
Dan Helfman 620f9e64d6 A few more tests for new colored logging. 2019-05-13 20:49:20 -07:00
Dan Helfman 25c320b281 Pin pip version: cherrypick of https://projects.torsion.org/witten/borgmatic/pulls/172 2019-05-13 20:01:25 -07:00
Dan Helfman f19eec56ac Add tox pin to NEWS. 2019-05-13 17:07:20 -07:00
Dan HelfmanandGitea 7cbcff2e9b Pin tox version. 2019-05-14 00:06:07 +00:00
Dan Helfman 9f6407ada6 Mention continuous integration badge in NEWS. 2019-05-13 14:54:24 -07:00
Dan HelfmanandGitea e933ecf046 Add drone CI note and badge. 2019-05-13 21:51:00 +00:00
Luke Murphy 4010a2ed77 Add note about Drone CI to contributing docs 2019-05-13 23:43:11 +02:00
Luke Murphy 2f36096e1a Add Drone build badge 2019-05-13 23:42:11 +02:00
Luke Murphy 82ec45e375 Pin tox version
Towards more reproducible results with Tox.
2019-05-13 23:12:18 +02:00
Dan Helfman 37362150fe Color records that are logged via logger.handle() as well. 2019-05-13 13:50:32 -07:00
Dan Helfman a7ba97803f Add colored output to NEWS. 2019-05-13 19:52:54 +00:00
Dan HelfmanandGitea 31dc903877 Integrate colorama for colored output. 2019-05-13 19:50:36 +00:00
Dan Helfman 8943867433 Bump to dev version. 2019-05-13 19:07:27 +00:00
Dan Helfman d9cb110563 Document installing borgmatic with pip install --user instead of a system Python install. 2019-05-13 19:06:42 +00:00
Dan HelfmanandGitea 32113cee67 Document installing borgmatic with pip install --user instead of a system Python install. 2019-05-13 19:04:24 +00:00
Luke Murphy a621ce199a Add tests for borgmatic.logger.to_bool 2019-05-13 13:40:23 +02:00
Luke Murphy 1f524d6c87 Add borgmatic custom logger 2019-05-13 13:40:18 +02:00
Luke Murphy 0320d449ec Add documentation about colorama 2019-05-13 13:40:17 +02:00
Luke Murphy 30f007687a Add colorama to testing dependencies 2019-05-13 13:40:17 +02:00
Luke Murphy adf7856162 Add new colorama dependency 2019-05-13 13:40:17 +02:00
Luke Murphy f9dce8b2d3 Recommend user installs when upgrading 2019-05-13 13:18:59 +02:00
Luke Murphy 15cb6270ef Recommend a tox user install for developing 2019-05-13 13:18:45 +02:00
Luke Murphy ed14fdbac9 Recommend root user package install
This can do bad things to a system Python install. So, we try to
mitigate this by recommending a root user user site installation.
2019-05-13 13:18:37 +02:00
Dan Helfman 8650a15db1 Document validate-borgmatic-config and add a few tests. 2019-05-11 14:05:16 -07:00
Dan HelfmanandGitea 6a10022543 Add validate-borgmatic-config command. 2019-05-11 20:15:06 +00:00
Robin Schneider 52e4f48eb9 Add validate-borgmatic-config command
Useful when generating the borgmatic configuration file with
configuration management and before moving the generated file in place
checking if it is actually valid.
2019-05-10 00:10:28 +02:00
Dan Helfman f5e1e8bec9 In continuous integration build matrix, use newer Alpine 3.9 instead of 3.8. 2019-05-07 16:19:03 -07:00
Dan Helfman a291477c19 Fix for hooks executing when using --dry-run (#160). 2019-05-07 16:06:31 -07:00
95 changed files with 2691 additions and 570 deletions
+56 -17
View File
@@ -1,18 +1,57 @@
pipeline:
build:
image: python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
pull: true
commands:
- pip install tox
- tox
- apk add --no-cache borgbackup
- tox -e end-to-end
---
kind: pipeline
name: python-3-5-alpine-3-9
matrix:
ALPINE_VERSION:
- 3.7
- 3.8
PYTHON_VERSION:
- 3.5
- 3.6
- 3.7
steps:
- name: build
image: python:3.5-alpine3.9
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-6-alpine-3-9
steps:
- name: build
image: python:3.6-alpine3.9
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-7-alpine-3-9
steps:
- name: build
image: python:3.7-alpine3.9
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: python-3-7-alpine-3-7
steps:
- name: build
image: python:3.7-alpine3.7
pull: always
commands:
- scripts/run-tests
---
kind: pipeline
name: documentation
steps:
- name: build
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: witten/borgmatic-docs
dockerfile: docs/Dockerfile
when:
branch:
- master
+41
View File
@@ -0,0 +1,41 @@
const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(pluginSyntaxHighlight);
eleventyConfig.addPlugin(inclusiveLangPlugin);
let markdownIt = require("markdown-it");
let markdownItAnchor = require("markdown-it-anchor");
let markdownItReplaceLink = require("markdown-it-replace-link");
let markdownItOptions = {
html: true,
breaks: false,
linkify: true,
// Replace links to .md files with links to directories. This allows unparsed Markdown links
// to work on GitHub, while rendered links elsewhere also work.
replaceLink: function (link, env) {
return link.replace(/\.md$/, '/');
}
};
let markdownItAnchorOptions = {
permalink: true,
permalinkClass: "direct-link"
};
eleventyConfig.setLibrary(
"md",
markdownIt(markdownItOptions)
.use(markdownItAnchor, markdownItAnchorOptions)
.use(markdownItReplaceLink)
);
return {
templateFormats: [
"md",
"txt"
]
}
};
+31
View File
@@ -0,0 +1,31 @@
#### What I'm trying to do and why
#### Steps to reproduce (if a bug)
Include (sanitized) borgmatic configuration files if applicable.
#### Actual behavior (if a bug)
Include (sanitized) `--verbosity 2` output if applicable.
#### Expected behavior (if a bug)
#### Other notes / implementation ideas
#### Environment
**borgmatic version:** [version here]
Use `sudo borgmatic --version` or `sudo pip show borgmatic | grep ^Version`
**borgmatic installation method:** [e.g., Debian package, Docker container, etc.]
**Borg version:** [version here]
Use `sudo borg --version`
**Python version:** [version here]
Use `python --version`
**operating system and version:** [OS here]
+3 -2
View File
@@ -5,5 +5,6 @@
.coverage
.pytest_cache
.tox
build
dist
build/
dist/
pip-wheel-metadata/
+1 -1
View File
@@ -7,6 +7,6 @@ Johannes Feichtner: Support for user hooks
Michele Lazzeri: Custom archive names
Nick Whyte: Support prefix filtering for archive consistency checks
newtonne: Read encryption password from external file
Robin `ypid` Schneider: Support additional options of Borg
Robin `ypid` Schneider: Support additional options of Borg and add validate-borgmatic-config command
Scott Squires: Custom archive names
Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option
+43
View File
@@ -1,3 +1,46 @@
1.3.8
* #191: Disable console color via "color" option in borgmatic configuration output section.
1.3.7
* #196: Fix for unclear error message for invalid YAML merge include.
* #197: Don't color syslog output.
* Change default syslog verbosity to show errors only.
1.3.6
* #53: Log to syslog in addition to existing console logging. Add --syslog-verbosity flag to
customize the log level. See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/
* #178: Look for .yml configuration file extension in addition to .yaml.
* #189: Set umask used when executing hooks via "umask" option in borgmatic hooks section.
* Remove Python cache files before each Tox run.
* Add #borgmatic Freenode IRC channel to documentation.
* Add Borg/borgmatic hosting providers section to documentation.
* Add files for building documentation into a Docker image for web serving.
* Upgrade project build server from Drone 0.8 to 1.1.
* Build borgmatic documentation during continuous integration.
* We're nearly at 500 ★s on GitHub. We can do this!
1.3.5
* #153: Support for various Borg directory environment variables (BORG_CONFIG_DIR, BORG_CACHE_DIR,
etc.) via options in borgmatic's storage configuration.
* #177: Fix for regression with missing verbose log entries.
1.3.4
* Part of #125: Color borgmatic (but not Borg) output when using an interactive terminal.
* #166: Run tests for all installed versions of Python.
* #168: Update README with continuous integration badge.
* #169: Automatically sort Python imports in code.
* Document installing borgmatic with pip install --user instead of a system Python install.
* Get more reproducible builds by pinning the versions of pip and tox used to run tests.
* Factor out build/test configuration from tox.ini file.
1.3.3
* Add validate-borgmatic-config command, useful for validating borgmatic config generated by
configuration management or even edited by hand.
1.3.2
* #160: Fix for hooks executing when using --dry-run. Now hooks are skipped during a dry run.
1.3.1
* #155: Fix for invalid JSON output when using multiple borgmatic configuration files.
* #157: Fix for seemingly random filename ordering when running through a directory of
+45 -20
View File
@@ -1,17 +1,20 @@
---
title: borgmatic
permalink: borgmatic/index.html
permalink: index.html
---
<a href="https://build.torsion.org/witten/borgmatic" alt="build status">![Build Status](https://build.torsion.org/api/badges/witten/borgmatic/status.svg)</a>
## Overview
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" width="150px" style="float: right; padding-left: 1em;">
<img src="https://projects.torsion.org/witten/borgmatic/raw/branch/master/static/borgmatic.png" alt="borgmatic logo" width="150px" style="float: right; padding-left: 1em;">
borgmatic is a simple Python wrapper script for the
[Borg](https://www.borgbackup.org/) backup software that initiates a backup,
prunes any old backups according to a retention policy, and validates backups
for consistency. The script supports specifying your settings in a declarative
configuration file rather than having to put them all on the command-line, and
handles common errors.
borgmatic is simple, configuration-driven backup software for servers and
workstations. Backup all of your machines from the command-line or scheduled
jobs. No GUI required. Built atop [Borg Backup](https://www.borgbackup.org/),
borgmatic initiates a backup, prunes any old backups according to a retention
policy, and validates backups for consistency. borgmatic supports specifying
your settings in a declarative configuration file, rather than having to put
them all on the command-line, and handles common errors.
Here's an example config file:
@@ -56,22 +59,34 @@ href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
## How-to guides
* [Set up backups with borgmatic](docs/how-to/set-up-backups.md) ⬅ *Start here!*
* [Make per-application backups](docs/how-to/make-per-application-backups.md)
* [Deal with very large backups](docs/how-to/deal-with-very-large-backups.md)
* [Inspect your backups](docs/how-to/inspect-your-backups.md)
* [Restore a backup](docs/how-to/restore-a-backup.md)
* [Run preparation steps before backups](docs/how-to/run-preparation-steps-before-backups.md)
* [Upgrade borgmatic](docs/how-to/upgrade.md)
* [Develop on borgmatic](docs/how-to/develop-on-borgmatic.md)
* [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/)
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/)
* [Run preparation steps before backups](https://torsion.org/borgmatic/docs/how-to/run-preparation-steps-before-backups/)
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
## Reference guides
* [borgmatic configuration reference](docs/reference/configuration.md)
* [borgmatic command-line reference](docs/reference/command-line.md)
* [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 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>
## Support and contributing
### Issues
@@ -82,6 +97,11 @@ 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 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>.
Other questions or comments? Contact <mailto:witten@torsion.org>.
@@ -94,5 +114,10 @@ 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!
Also, please check out the [borgmatic development
how-to](docs/how-to/develop-on-borgmatic.md) for info on cloning source code,
running tests, etc.
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
info on cloning source code, running tests, etc.
<script>
var links = document.getElementsByClassName("referral");
links[Math.floor(Math.random() * links.length)].style.display = "none";
</script>
+2 -8
View File
@@ -1,9 +1,7 @@
import logging
import os
import subprocess
from borgmatic.borg import extract
from borgmatic.execute import execute_command
DEFAULT_CHECKS = ('repository', 'archives')
DEFAULT_PREFIX = '{hostname}-'
@@ -114,11 +112,7 @@ def check_archives(
+ verbosity_flags
)
# The check command spews to stdout/stderr even without the verbose flag. Suppress it.
stdout = None if verbosity_flags else open(os.devnull, 'w')
logger.debug(' '.join(full_command))
subprocess.check_call(full_command, stdout=stdout, stderr=subprocess.STDOUT)
execute_command(full_command)
if 'extract' in checks:
extract.extract_last_archive_dry_run(repository, lock_wait, local_path, remote_path)
+17 -7
View File
@@ -4,8 +4,7 @@ import logging
import os
import tempfile
from borgmatic.borg.execute import execute_command
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -149,13 +148,24 @@ def create_archive(
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--umask', str(umask)) if umask else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--stats',) if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) and not json else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (
('--stats',)
if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) and not json
else ()
)
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--dry-run',) if dry_run else ())
+ (('--progress',) if progress else ())
+ (('--json',) if json else ())
)
return execute_command(full_command, capture_output=json)
if json:
output_log_level = None
elif stats:
output_log_level = logging.WARNING
else:
output_log_level = logging.INFO
return execute_command(full_command, output_log_level)
+15 -11
View File
@@ -1,15 +1,19 @@
import os
OPTION_TO_ENVIRONMENT_VARIABLE = {
'borg_base_directory': 'BORG_BASE_DIR',
'borg_config_directory': 'BORG_CONFIG_DIR',
'borg_cache_directory': 'BORG_CACHE_DIR',
'borg_security_directory': 'BORG_SECURITY_DIR',
'borg_keys_directory': 'BORG_KEYS_DIR',
'encryption_passcommand': 'BORG_PASSCOMMAND',
'encryption_passphrase': 'BORG_PASSPHRASE',
'ssh_command': 'BORG_RSH',
}
def initialize(storage_config):
passcommand = storage_config.get('encryption_passcommand')
if passcommand:
os.environ['BORG_PASSCOMMAND'] = passcommand
passphrase = storage_config.get('encryption_passphrase')
if passphrase:
os.environ['BORG_PASSPHRASE'] = passphrase
ssh_command = storage_config.get('ssh_command')
if ssh_command:
os.environ['BORG_RSH'] = ssh_command
for option_name, environment_variable_name in OPTION_TO_ENVIRONMENT_VARIABLE.items():
value = storage_config.get(option_name)
if value:
os.environ[environment_variable_name] = value
-19
View File
@@ -1,19 +0,0 @@
import logging
import subprocess
logger = logging.getLogger(__name__)
def execute_command(full_command, capture_output=False):
'''
Execute the given command (a sequence of command/argument strings). If capture output is True,
then return the command's output as a string.
'''
logger.debug(' '.join(full_command))
if capture_output:
output = subprocess.check_output(full_command)
return output.decode() if output is not None else None
else:
subprocess.check_call(full_command)
+7 -9
View File
@@ -1,7 +1,6 @@
import logging
import sys
import subprocess
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -26,10 +25,11 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
+ verbosity_flags
)
list_output = subprocess.check_output(full_list_command).decode(sys.stdout.encoding)
list_output = execute_command(full_list_command, output_log_level=None)
last_archive_name = list_output.strip().split('\n')[-1]
if not last_archive_name:
try:
last_archive_name = list_output.strip().splitlines()[-1]
except IndexError:
return
list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
@@ -48,8 +48,7 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
+ list_flag
)
logger.debug(' '.join(full_extract_command))
subprocess.check_call(full_extract_command)
execute_command(full_extract_command)
def extract_archive(
@@ -84,5 +83,4 @@ def extract_archive(
+ (('--progress',) if progress else ())
)
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
execute_command(full_command)
+4 -5
View File
@@ -1,7 +1,6 @@
import logging
from borgmatic.borg.execute import execute_command
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -19,9 +18,9 @@ def display_archives_info(
(local_path, 'info', repository)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--json',) if json else ())
)
return execute_command(full_command, capture_output=json)
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
+10 -2
View File
@@ -1,10 +1,14 @@
import logging
import subprocess
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
INFO_REPOSITORY_NOT_FOUND_EXIT_CODE = 2
def initialize_repository(
repository,
encryption_mode,
@@ -21,9 +25,13 @@ def initialize_repository(
info_command = (local_path, 'info', repository)
logger.debug(' '.join(info_command))
if subprocess.call(info_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
try:
execute_command(info_command, output_log_level=None)
logger.info('Repository already exists. Skipping initialization.')
return
except subprocess.CalledProcessError as error:
if error.returncode != INFO_REPOSITORY_NOT_FOUND_EXIT_CODE:
raise
init_command = (
(local_path, 'init', repository)
@@ -35,5 +43,5 @@ def initialize_repository(
+ (('--remote-path', remote_path) if remote_path else ())
)
logger.debug(' '.join(init_command))
# Don't use execute_command() here because it doesn't support interactive prompts.
subprocess.check_call(init_command)
+4 -5
View File
@@ -1,7 +1,6 @@
import logging
from borgmatic.borg.execute import execute_command
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -20,9 +19,9 @@ def list_archives(
(local_path, 'list', '::'.join((repository, archive)) if archive else repository)
+ (('--remote-path', remote_path) if remote_path else ())
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
+ (('--json',) if json else ())
)
return execute_command(full_command, capture_output=json)
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
+2 -3
View File
@@ -1,6 +1,6 @@
import logging
import subprocess
from borgmatic.execute import execute_command
logger = logging.getLogger(__name__)
@@ -60,5 +60,4 @@ def prune_archives(
+ (('--stats',) if stats else ())
)
logger.debug(' '.join(full_command))
subprocess.check_call(full_command)
execute_command(full_command)
+105 -43
View File
@@ -1,32 +1,30 @@
from argparse import ArgumentParser
import collections
import json
import logging
import os
from subprocess import CalledProcessError
import sys
from argparse import ArgumentParser
from subprocess import CalledProcessError
import colorama
import pkg_resources
from borgmatic.borg import (
check as borg_check,
create as borg_create,
environment as borg_environment,
prune as borg_prune,
extract as borg_extract,
list as borg_list,
info as borg_info,
init as borg_init,
)
from borgmatic.commands import hook
from borgmatic import hook
from borgmatic.borg import check as borg_check
from borgmatic.borg import create as borg_create
from borgmatic.borg import environment as borg_environment
from borgmatic.borg import extract as borg_extract
from borgmatic.borg import info as borg_info
from borgmatic.borg import init as borg_init
from borgmatic.borg import list as borg_list
from borgmatic.borg import prune as borg_prune
from borgmatic.config import checks, collect, convert, validate
from borgmatic.logger import configure_logging, should_do_markup
from borgmatic.signals import configure_signals
from borgmatic.verbosity import verbosity_to_log_level
logger = logging.getLogger(__name__)
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
@@ -169,13 +167,23 @@ def parse_arguments(*arguments):
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
common_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
)
common_group.add_argument(
'-v',
'--verbosity',
type=int,
choices=range(0, 3),
default=0,
help='Display verbose progress (1 for some, 2 for lots)',
help='Display verbose progress to the console (from none to lots: 0, 1, or 2)',
)
common_group.add_argument(
'--syslog-verbosity',
type=int,
choices=range(0, 3),
default=0,
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)',
)
common_group.add_argument(
'--version',
@@ -272,7 +280,13 @@ def run_configuration(config_filename, config, args): # pragma: no cover
borg_environment.initialize(storage)
if args.create:
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
hook.execute_hook(
hooks.get('before_backup'),
hooks.get('umask'),
config_filename,
'pre-backup',
args.dry_run,
)
for repository_path in location['repositories']:
yield from run_actions(
@@ -287,9 +301,17 @@ def run_configuration(config_filename, config, args): # pragma: no cover
)
if args.create:
hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup')
hook.execute_hook(
hooks.get('after_backup'),
hooks.get('umask'),
config_filename,
'post-backup',
args.dry_run,
)
except (OSError, CalledProcessError):
hook.execute_hook(hooks.get('on_error'), config_filename, 'on-error')
hook.execute_hook(
hooks.get('on_error'), hooks.get('umask'), config_filename, 'on-error', args.dry_run
)
raise
@@ -382,40 +404,58 @@ def run_actions(
yield json.loads(json_output)
def collect_configuration_run_summary_logs(config_filenames, args):
def load_configurations(config_filenames):
'''
Given a sequence of configuration filenames and parsed command-line arguments as an
argparse.ArgumentParser instance, run each configuration file and yield a series of
logging.LogRecord instances containing summary information about each run.
As a side effect of running through these configuration files, output their JSON results, if
any, to stdout.
Given a sequence of configuration filenames, load and validate each configuration file. Return
the results as a tuple of: dict of configuration filename to corresponding parsed configuration,
and sequence of logging.LogRecord instances containing any parse errors.
'''
# Dict mapping from config filename to corresponding parsed config dict.
configs = collections.OrderedDict()
logs = []
# Parse and load each configuration file.
for config_filename in config_filenames:
try:
logger.info('{}: Parsing configuration file'.format(config_filename))
configs[config_filename] = validate.parse_configuration(
config_filename, validate.schema_filename()
)
except (ValueError, OSError, validate.Validation_error) as error:
yield logging.makeLogRecord(
dict(
levelno=logging.CRITICAL,
msg='{}: Error parsing configuration file'.format(config_filename),
)
logs.extend(
[
logging.makeLogRecord(
dict(
levelno=logging.CRITICAL,
levelname='CRITICAL',
msg='{}: Error parsing configuration file'.format(config_filename),
)
),
logging.makeLogRecord(
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
),
]
)
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error))
return (configs, logs)
def collect_configuration_run_summary_logs(configs, args):
'''
Given a dict of configuration filename to corresponding parsed configuration, and parsed
command-line arguments as an argparse.ArgumentParser instance, run each configuration file and
yield a series of logging.LogRecord instances containing summary information about each run.
As a side effect of running through these configuration files, output their JSON results, if
any, to stdout.
'''
# Run cross-file validation checks.
if args.extract or (args.list and args.archive):
try:
validate.guard_configuration_contains_repository(args.repository, configs)
except ValueError as error:
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error))
yield logging.makeLogRecord(
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
)
return
# Execute the actions corresponding to each configuration file.
@@ -426,6 +466,7 @@ def collect_configuration_run_summary_logs(config_filenames, args):
yield logging.makeLogRecord(
dict(
levelno=logging.INFO,
levelname='INFO',
msg='{}: Successfully ran configuration file'.format(config_filename),
)
)
@@ -433,18 +474,22 @@ def collect_configuration_run_summary_logs(config_filenames, args):
yield logging.makeLogRecord(
dict(
levelno=logging.CRITICAL,
levelname='CRITICAL',
msg='{}: Error running configuration file'.format(config_filename),
)
)
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error))
yield logging.makeLogRecord(
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
)
if json_results:
sys.stdout.write(json.dumps(json_results))
if not config_filenames:
if not configs:
yield logging.makeLogRecord(
dict(
levelno=logging.CRITICAL,
levelname='CRITICAL',
msg='{}: No configuration files found'.format(' '.join(args.config_paths)),
)
)
@@ -454,7 +499,8 @@ def exit_with_help_link(): # pragma: no cover
'''
Display a link to get help and exit with an error code.
'''
logger.critical('\nNeed some help? https://torsion.org/borgmatic/#issues')
logger.critical('')
logger.critical('Need some help? https://torsion.org/borgmatic/#issues')
sys.exit(1)
@@ -464,24 +510,40 @@ def main(): # pragma: no cover
try:
args = parse_arguments(*sys.argv[1:])
except ValueError as error:
logging.basicConfig(level=logging.CRITICAL, format='%(message)s')
configure_logging(logging.CRITICAL)
logger.critical(error)
exit_with_help_link()
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
except SystemExit as error:
if error.code == 0:
raise error
configure_logging(logging.CRITICAL)
logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
exit_with_help_link()
if args.version:
print(pkg_resources.require('borgmatic')[0].version)
sys.exit(0)
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
configs, parse_logs = load_configurations(config_filenames)
colorama.init(autoreset=True, strip=not should_do_markup(args.no_color, configs))
configure_logging(
verbosity_to_log_level(args.verbosity), verbosity_to_log_level(args.syslog_verbosity)
)
logger.debug('Ensuring legacy configuration is upgraded')
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
summary_logs = tuple(collect_configuration_run_summary_logs(config_filenames, args))
summary_logs = list(collect_configuration_run_summary_logs(configs, args))
logger.info('\nsummary:')
[logger.handle(log) for log in summary_logs if log.levelno >= logger.getEffectiveLevel()]
logger.info('')
logger.info('summary:')
[
logger.handle(log)
for log in parse_logs + summary_logs
if log.levelno >= logger.getEffectiveLevel()
]
if any(log.levelno == logging.CRITICAL for log in summary_logs):
exit_with_help_link()
+1 -2
View File
@@ -1,13 +1,12 @@
from argparse import ArgumentParser
import os
import sys
import textwrap
from argparse import ArgumentParser
from ruamel import yaml
from borgmatic.config import convert, generate, legacy, validate
DEFAULT_SOURCE_CONFIG_FILENAME = '/etc/borgmatic/config'
DEFAULT_SOURCE_EXCLUDES_FILENAME = '/etc/borgmatic/excludes'
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
+1 -2
View File
@@ -1,9 +1,8 @@
from argparse import ArgumentParser
import sys
from argparse import ArgumentParser
from borgmatic.config import generate, validate
DEFAULT_DESTINATION_CONFIG_FILENAME = '/etc/borgmatic/config.yaml'
-24
View File
@@ -1,24 +0,0 @@
import logging
import subprocess
logger = logging.getLogger(__name__)
def execute_hook(commands, config_filename, description):
if not commands:
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
return
if len(commands) == 1:
logger.info('{}: Running command for {} hook'.format(config_filename, description))
else:
logger.info(
'{}: Running {} commands for {} hook'.format(
config_filename, len(commands), description
)
)
for command in commands:
logger.debug('{}: Hook command: {}'.format(config_filename, command))
subprocess.check_call(command, shell=True)
+56
View File
@@ -0,0 +1,56 @@
import logging
import sys
from argparse import ArgumentParser
from borgmatic.config import collect, validate
logger = logging.getLogger(__name__)
def parse_arguments(*arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
them as an ArgumentParser instance.
'''
config_paths = collect.get_default_config_paths()
parser = ArgumentParser(description='Validate borgmatic configuration file(s).')
parser.add_argument(
'-c',
'--config',
nargs='+',
dest='config_paths',
default=config_paths,
help='Configuration filenames or directories, defaults to: {}'.format(
' '.join(config_paths)
),
)
return parser.parse_args(arguments)
def main(): # pragma: no cover
args = parse_arguments(*sys.argv[1:])
logging.basicConfig(level=logging.INFO, format='%(message)s')
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
if len(config_filenames) == 0:
logger.critical('No files to validate found')
sys.exit(1)
found_issues = False
for config_filename in config_filenames:
try:
validate.parse_configuration(config_filename, validate.schema_filename())
except (ValueError, OSError, validate.Validation_error) as error:
logging.critical('{}: Error parsing configuration file'.format(config_filename))
logging.critical(error)
found_issues = True
if found_issues:
sys.exit(1)
else:
logger.info(
'All given configuration files are valid: {}'.format(', '.join(config_filenames))
)
+3 -2
View File
@@ -22,7 +22,7 @@ def collect_config_filenames(config_paths):
'''
Given a sequence of config paths, both filenames and directories, resolve that to an iterable
of files. Accomplish this by listing any given directories looking for contained config files
(ending with the ".yaml" extension). This is non-recursive, so any directories within the given
(ending with the ".yaml" or ".yml" extension). This is non-recursive, so any directories within the given
directories are ignored.
Return paths even if they don't exist on disk, so the user can find out about missing
@@ -43,5 +43,6 @@ def collect_config_filenames(config_paths):
for filename in sorted(os.listdir(path)):
full_filename = os.path.join(path, filename)
if full_filename.endswith('.yaml') and not os.path.isdir(full_filename):
matching_filetype = full_filename.endswith('.yaml') or full_filename.endswith('.yml')
if matching_filetype and not os.path.isdir(full_filename):
yield full_filename
+4 -4
View File
@@ -64,9 +64,9 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
return destination_config
class LegacyConfigurationNotUpgraded(FileNotFoundError):
class Legacy_configuration_not_upgraded(FileNotFoundError):
def __init__(self):
super(LegacyConfigurationNotUpgraded, self).__init__(
super(Legacy_configuration_not_upgraded, self).__init__(
'''borgmatic changed its configuration file format in version 1.1.0 from INI-style
to YAML. This better supports validation, and has a more natural way to express
lists of values. To upgrade your existing configuration, run:
@@ -83,7 +83,7 @@ instead of the old one.'''
def guard_configuration_upgraded(source_config_filename, destination_config_filenames):
'''
If legacy source configuration exists but no destination upgraded configs do, raise
LegacyConfigurationNotUpgraded.
Legacy_configuration_not_upgraded.
The idea is that we want to alert the user about upgrading their config if they haven't already.
'''
@@ -92,4 +92,4 @@ def guard_configuration_upgraded(source_config_filename, destination_config_file
)
if os.path.exists(source_config_filename) and not destination_config_exists:
raise LegacyConfigurationNotUpgraded()
raise Legacy_configuration_not_upgraded()
-1
View File
@@ -2,7 +2,6 @@ import os
from ruamel import yaml
INDENT = 4
-1
View File
@@ -1,7 +1,6 @@
from collections import OrderedDict, namedtuple
from configparser import RawConfigParser
Section_format = namedtuple('Section_format', ('name', 'options'))
Config_option = namedtuple('Config_option', ('name', 'value_type', 'required'))
+2 -5
View File
@@ -1,9 +1,8 @@
import os
import logging
import os
import ruamel.yaml
logger = logging.getLogger(__name__)
@@ -54,9 +53,7 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
for index, (key_node, value_node) in enumerate(node.value):
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
included_value = representer.represent_mapping(
tag='tag:yaml.org,2002:map', mapping=self.construct_object(value_node)
)
included_value = representer.represent_data(self.construct_object(value_node))
node.value[index] = (key_node, included_value)
super(Include_constructor, self).flatten_mapping(node)
+44 -2
View File
@@ -156,7 +156,7 @@ map:
desc: |
Type of compression to use when creating archives. See
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details.
Defaults to no compression.
Defaults to "lz4".
example: lz4
remote_rate_limit:
type: int
@@ -168,6 +168,32 @@ map:
Command to use instead of "ssh". This can be used to specify ssh options.
Defaults to not set.
example: ssh -i /path/to/private/key
borg_base_directory:
type: scalar
desc: |
Base path used for various Borg directories. Defaults to $HOME, ~$USER, or ~.
See https://borgbackup.readthedocs.io/en/stable/usage/general.html#environment-variables for details.
example: /path/to/base
borg_config_directory:
type: scalar
desc: |
Path for Borg configuration files. Defaults to $borg_base_directory/.config/borg
example: /path/to/base/config
borg_cache_directory:
type: scalar
desc: |
Path for Borg cache files. Defaults to $borg_base_directory/.cache/borg
example: /path/to/base/cache
borg_security_directory:
type: scalar
desc: |
Path for Borg security and encryption nonce files. Defaults to $borg_base_directory/.config/borg/security
example: /path/to/base/config/security
borg_keys_directory:
type: scalar
desc: |
Path for Borg encryption key files. Defaults to $borg_base_directory/.config/borg/keys
example: /path/to/base/config/keys
umask:
type: scalar
desc: Umask to be used for borg create. Defaults to 0077.
@@ -190,7 +216,9 @@ map:
desc: |
Retention policy for how many backups to keep in each category. See
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-prune for details.
At least one of the "keep" options is required for pruning to work.
At least one of the "keep" options is required for pruning to work. See
https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/
if you'd like to skip pruning entirely.
map:
keep_within:
type: scalar
@@ -273,6 +301,16 @@ map:
this prefix. Borg placeholders can be used. See the output of
"borg help placeholders" for details. Defaults to "{hostname}-".
example: sourcehostname
output:
desc: |
Options for customizing borgmatic's own output and logging.
map:
color:
type: bool
desc: |
Apply color to console output. Can be overridden with --no-color command-line
flag. Defaults to true.
example: false
hooks:
desc: |
Shell commands or scripts to execute before and after a backup or if an error has occurred.
@@ -298,3 +336,7 @@ map:
desc: List of one or more shell commands or scripts to execute in case an exception has occurred.
example:
- echo "Error while creating a backup."
umask:
type: scalar
desc: Umask used when executing hooks. Defaults to the umask that borgmatic is run with.
example: 0077
-10
View File
@@ -8,9 +8,6 @@ import ruamel.yaml
from borgmatic.config import load
logger = logging.getLogger(__name__)
def schema_filename():
'''
Path to the installed YAML configuration schema file, used to validate and parse the
@@ -66,13 +63,6 @@ def apply_logical_validation(config_filename, parsed_configuration):
),
)
consistency_prefix = parsed_configuration.get('consistency', {}).get('prefix')
if archive_name_format and not consistency_prefix:
logger.warning(
'Since version 1.1.16, if you provide `archive_name_format`, you should also'
' specify `consistency.prefix`.'
)
def parse_configuration(config_filename, schema_filename):
'''
+43
View File
@@ -0,0 +1,43 @@
import logging
import subprocess
logger = logging.getLogger(__name__)
def execute_and_log_output(full_command, output_log_level, shell):
process = subprocess.Popen(
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
)
while process.poll() is None:
line = process.stdout.readline().rstrip().decode()
if not line:
continue
if line.startswith('borg: error:'):
logger.error(line)
else:
logger.log(output_log_level, line)
remaining_output = process.stdout.read().rstrip().decode()
if remaining_output: # pragma: no cover
logger.log(output_log_level, remaining_output)
exit_code = process.poll()
if exit_code != 0:
raise subprocess.CalledProcessError(exit_code, full_command)
def execute_command(full_command, output_log_level=logging.INFO, shell=False):
'''
Execute the given command (a sequence of command/argument strings) and log its output at the
given log level. If output log level is None, instead capture and return the output. If
shell is True, execute the command within a shell.
'''
logger.debug(' '.join(full_command))
if output_log_level is None:
output = subprocess.check_output(full_command, shell=shell)
return output.decode() if output is not None else None
else:
execute_and_log_output(full_command, output_log_level, shell=shell)
+53
View File
@@ -0,0 +1,53 @@
import logging
import os
from borgmatic import execute
logger = logging.getLogger(__name__)
def execute_hook(commands, umask, config_filename, description, dry_run):
'''
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
a hook description, and whether this is a dry run, run the given commands. Or, don't run them
if this is a dry run.
Raise ValueError if the umask cannot be parsed.
'''
if not commands:
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
return
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
if len(commands) == 1:
logger.info(
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
)
else:
logger.info(
'{}: Running {} commands for {} hook{}'.format(
config_filename, len(commands), description, dry_run_label
)
)
if umask:
parsed_umask = int(str(umask), 8)
logger.debug('{}: Set hook umask to {}'.format(config_filename, oct(parsed_umask)))
original_umask = os.umask(parsed_umask)
else:
original_umask = None
try:
for command in commands:
if not dry_run:
execute.execute_command(
[command],
output_log_level=logging.ERROR
if description == 'on-error'
else logging.WARNING,
shell=True,
)
finally:
if original_umask:
os.umask(original_umask)
+93
View File
@@ -0,0 +1,93 @@
import logging
import os
import sys
import colorama
def to_bool(arg):
'''
Return a boolean value based on `arg`.
'''
if arg is None or isinstance(arg, bool):
return arg
if isinstance(arg, str):
arg = arg.lower()
if arg in ('yes', 'on', '1', 'true', 1):
return True
return False
def should_do_markup(no_color, configs):
'''
Given the value of the command-line no-color argument, and a dict of configuration filename to
corresponding parsed configuration, determine if we should enable colorama marking up.
'''
if no_color:
return False
if any(config.get('output', {}).get('color') is False for config in configs.values()):
return False
py_colors = os.environ.get('PY_COLORS', None)
if py_colors is not None:
return to_bool(py_colors)
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
LOG_LEVEL_TO_COLOR = {
logging.CRITICAL: colorama.Fore.RED,
logging.ERROR: colorama.Fore.RED,
logging.WARN: colorama.Fore.YELLOW,
logging.INFO: colorama.Fore.GREEN,
logging.DEBUG: colorama.Fore.CYAN,
}
class Console_color_formatter(logging.Formatter):
def format(self, record):
color = LOG_LEVEL_TO_COLOR.get(record.levelno)
return color_text(color, record.msg)
def color_text(color, message):
'''
Give colored text.
'''
if not color:
return message
return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
def configure_logging(console_log_level, syslog_log_level=None):
'''
Configure logging to go to both the console and syslog. Use the given log levels, respectively.
'''
if syslog_log_level is None:
syslog_log_level = console_log_level
console_handler = logging.StreamHandler()
console_handler.setFormatter(Console_color_formatter())
console_handler.setLevel(console_log_level)
syslog_path = None
if os.path.exists('/dev/log'):
syslog_path = '/dev/log'
elif os.path.exists('/var/run/syslog'):
syslog_path = '/var/run/syslog'
if syslog_path:
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s \ufeff%(message)s'))
syslog_handler.setLevel(syslog_log_level)
handlers = (console_handler, syslog_handler)
else:
handlers = (console_handler,)
logging.basicConfig(level=min(console_log_level, syslog_log_level), handlers=handlers)
-1
View File
@@ -1,6 +1,5 @@
import logging
VERBOSITY_WARNING = 0
VERBOSITY_SOME = 1
VERBOSITY_LOTS = 2
+26
View File
@@ -0,0 +1,26 @@
FROM python:3.7.3-alpine3.9 as borgmatic
COPY . /app
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
RUN borgmatic --help > /command-line.txt
FROM node:11.15.0-alpine as html
WORKDIR /source
RUN npm install @11ty/eleventy \
@11ty/eleventy-plugin-syntaxhighlight \
@11ty/eleventy-plugin-inclusive-language \
markdown-it \
markdown-it-anchor \
markdown-it-replace-link
COPY --from=borgmatic /etc/borgmatic/config.yaml /source/docs/_includes/borgmatic/config.yaml
COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
COPY . /source
RUN npx eleventy --input=/source/docs --output=/output/docs \
&& mv /output/docs/index.html /output/index.html
FROM nginx:1.16.0-alpine
COPY --from=html /output /usr/share/nginx/html
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
+1
View File
@@ -0,0 +1 @@
../README.md
+1
View File
@@ -0,0 +1 @@
"layouts/main.njk"
+3
View File
@@ -0,0 +1,3 @@
.asciicast > iframe {
width: 100% !important;
}
@@ -0,0 +1,12 @@
/* External links */
a[href^="http://"]:not(.minilink):not(.elv-externalexempt),
a[href^="https://"]:not(.minilink):not(.elv-externalexempt),
a[href^="//"]:not(.minilink):not(.elv-externalexempt) {
text-decoration-color: inherit;
}
/* External link hovers */
a[href^="http://"]:not(.minilink):not(.elv-externalexempt):hover,
a[href^="https://"]:not(.minilink):not(.elv-externalexempt):hover,
a[href^="//"]:not(.minilink):not(.elv-externalexempt):hover {
text-decoration-color: #00bcd4;
}
+34
View File
@@ -0,0 +1,34 @@
/* Warning */
.elv-info {
line-height: 1.5;
padding: 0.8125em 1em 0.75em; /* 13px 16px 12px /16 */
margin-left: -1rem;
margin-right: -1rem;
margin-bottom: 2em;
background-color: #dff7ff;
}
.elv-info:before {
content: "️ ";
}
.elv-info-warn {
background-color: #ffa;
}
.elv-info-warn:before {
content: "⚠️ ";
}
.elv-info:first-child {
margin-top: 0;
}
body > .elv-info {
margin-left: 0;
margin-right: 0;
padding: .5rem 1rem;
}
@media (min-width: 37.5em) and (min-height: 25em) { /* 600px / 400px */
body > .elv-info-sticky {
position: sticky;
top: 0;
z-index: 2;
box-shadow: 0 3px 0 0 rgba(0,0,0,.08);
}
}
+126
View File
@@ -0,0 +1,126 @@
/* Buzzwords */
@keyframes rainbow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.buzzword-list,
.inlinelist {
padding: 0;
}
.inlinelist:first-child:last-child {
margin: 0;
}
.buzzword,
.buzzword-list li,
.inlinelist .inlinelist-item {
display: inline;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
font-family: Georgia, serif;
font-size: 116%;
white-space: normal;
line-height: 1.85;
padding: .2em .5em;
margin: 4px 4px 4px 0;
transition: .15s linear outline;
}
.inlinelist .inlinelist-item.active {
background-color: #222;
color: #fff;
font-weight: inherit;
}
.inlinelist .inlinelist-item.active :link,
.inlinelist .inlinelist-item.active :visited {
color: #fff;
}
.inlinelist .inlinelist-item code {
background-color: transparent;
}
a.buzzword {
text-decoration: underline;
}
.buzzword-list a,
.inlinelist a {
text-decoration: none;
}
.inlinelist .inlinelist-item {
font-size: 100%;
line-height: 2;
}
@supports not(-webkit-box-decoration-break: clone) {
.buzzword,
.buzzword-list li,
.inlinelist .inlinelist-item {
display: inline-block;
}
}
.buzzword-list li,
.buzzword {
background-color: #f7f7f7;
}
.inlinelist .inlinelist-item {
background-color: #e9e9e9;
}
.inlinelist .inlinelist-item:hover,
.inlinelist .inlinelist-item:focus,
.buzzword-list li:hover,
.buzzword-list li:focus,
.buzzword:hover,
.buzzword:focus {
position: relative;
background-image: linear-gradient(238deg, #ff0000, #ff8000, #ffff00, #80ff00, #00ff00, #00ff80, #00ffff, #0080ff, #0000ff, #8000ff, #ff0080);
background-size: 1200% 1200%;
color: #fff;
text-shadow: 0 0 2px rgba(0,0,0,.9);
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 {
color: #fff;
text-decoration: none;
}
/*
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 {
animation: rainbow 1s infinite;
}
}*/
.buzzword-list li:hover:after,
.buzzword-list li:focus:after,
.buzzword:hover:after,
.buzzword:focus:after {
font-family: system-ui, sans-serif;
content: "Buzzword alert!!!";
position: absolute;
left: 0;
top: 0;
max-width: 8em;
color: #f00;
font-weight: 700;
text-transform: uppercase;
transform: rotate(-10deg) translate(-25%, -125%);
text-shadow: 1px 1px 5px rgba(0,0,0,.6);
line-height: 1.2;
pointer-events: none;
}
main h2 .buzzword,
main h3 .buzzword,
main p .buzzword {
padding: 0px 7px;
font-size: 1em; /* 18px /18 */
margin: 0;
line-height: 1.444444444444; /* 26px /18 */
font-family: inherit;
}
main h2 a.buzzword,
main h3 a.buzzword,
main p a.buzzword {
text-decoration: underline;
}
+40
View File
@@ -0,0 +1,40 @@
/* Mini link */
.minilink {
display: inline-block;
padding: .125em .375em;
text-transform: uppercase;
font-size: 0.875rem; /* 14px /16 */
text-decoration: none;
background-color: #ddd;
border-radius: 0.1875em; /* 3px /16 */
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, sans-serif;
}
.minilink[href] {
box-shadow: 0 1px 1px 0 rgba(0,0,0,.5);
}
.minilink[href]:hover,
.minilink[href]:focus {
background-color: #bbb;
}
pre + .minilink {
color: #fff;
border-radius: 0 0 0.2857142857143em 0.2857142857143em; /* 4px /14 */
float: right;
background-color: #444;
color: #fff;
}
pre[class*=language-] + .minilink {
position: relative;
top: -0.7142857142857em; /* -10px /14 */
}
p.minilink {
float: right;
margin-left: 2em;
margin-bottom: 2em;
}
.minilink + pre[class*=language-] {
clear: both;
}
+63
View File
@@ -0,0 +1,63 @@
.elv-toc {
font-size: 1rem; /* Reset */
}
@media (min-width: 64em) { /* 1024px */
.elv-toc {
position: absolute;
left: -17rem;
width: 16rem;
}
}
.elv-toc-list {
padding-left: 0;
padding-right: 0;
list-style: none;
}
/* Nested lists */
.elv-toc-list ul {
padding: 0;
display: none;
margin-bottom: 1.5em;
list-style: none;
}
.elv-toc-list ul li {
padding-left: 0.875em; /* 14px /16 */
}
@media (min-width: 64em) and (min-height: 48em) { /* 1024 x 768px */
.elv-toc-list ul {
display: block;
}
}
/* List items */
.elv-toc-list a:not(:hover) {
text-decoration: none;
}
.elv-toc-list li {
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;
}
/* Active links */
.elv-toc-list li.elv-toc-active > a {
font-weight: 700;
text-decoration: underline;
}
.elv-toc-active > a:after {
content: " ⬅";
line-height: .5;
}
/* Show only active nested lists */
.elv-toc-list ul.elv-toc-active,
.elv-toc-list li.elv-toc-active > ul {
display: block;
}
+3
View File
@@ -0,0 +1,3 @@
<header class="elv-layout elv-layout-full elv-header{% if headerClass %} {{ headerClass }}{% endif %}">
<h1 class="elv-hed">{{ title | safe }}</h1>
</header>
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
<!doctype html>
<html lang="en"{% if templateClass %} class="{{ templateClass }}"{% endif %}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ subtitle + ' - ' if subtitle}}{{ title }}</title>
{%- set css %}
{% include 'index.css' %}
{% include 'components/lists.css' %}
{% include 'components/external-links.css' %}
{% include 'components/minilink.css' %}
{% include 'components/toc.css' %}
{% include 'components/info-blocks.css' %}
{% include 'prism-theme.css' %}
{% include 'asciinema.css' %}
{% endset %}
<style>{{ css | safe }}</style>
{% if feedTitle and feedUrl %}
<link rel="alternate" href="{{ feedUrl }}" title="{{ feedTitle }}" type="application/atom+xml">
{% endif %}
</head>
<body>
{{ content | safe }}
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
---
layout: layouts/base.njk
templateClass: elv-default
headerClass: elv-header-default
---
{% include "header.njk" %}
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
<article>
{{ content | safe }}
</article>
</main>
+172
View File
@@ -0,0 +1,172 @@
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: #ABB2BF;
background: none;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #383e49;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #9aa2b1;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #282c34;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #5C6370;
}
.token.punctuation {
color: #abb2bf;
}
.token.selector,
.token.tag {
color: #e06c75;
}
.token.property,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.attr-name,
.token.deleted {
color: #d19a66;
}
.token.string,
.token.char,
.token.attr-value,
.token.builtin,
.token.inserted {
color: #98c379;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #56b6c2;
}
.token.atrule,
.token.keyword {
color: #e06c75;
}
.token.function {
color: #61afef;
}
.token.regex,
.token.important,
.token.variable {
color: #c678dd;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
pre.line-numbers {
position: relative;
padding-left: 3.8em;
counter-reset: linenumber;
}
pre.line-numbers > code {
position: relative;
}
.line-numbers .line-numbers-rows {
position: absolute;
pointer-events: none;
top: 0;
font-size: 100%;
left: -3.8em;
width: 3em; /* works for line-numbers below 1000 lines */
letter-spacing: -1px;
border-right: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.line-numbers-rows > span {
pointer-events: none;
display: block;
counter-increment: linenumber;
}
.line-numbers-rows > span:before {
content: counter(linenumber);
color: #5C6370;
display: block;
padding-right: 0.8em;
text-align: right;
}
+1 -1
View File
@@ -93,4 +93,4 @@ backups.
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
+19 -4
View File
@@ -41,7 +41,7 @@ you're in the `borgmatic/` working copy, install tox, which is used for
setting up testing environments:
```bash
sudo pip3 install tox
pip3 install --user tox
```
Finally, to actually run tests, run:
@@ -62,6 +62,14 @@ following:
tox -e black
```
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:
```bash
tox -e isort
```
### End-to-end tests
borgmatic additionally includes some end-to-end tests that integration test
@@ -87,11 +95,18 @@ the following deviations from it:
indentation with an opening delimeter.
borgmatic code uses the [Black](https://black.readthedocs.io/en/stable/) code
formatter and [Flake8](http://flake8.pycqa.org/en/latest/) code checker, so
formatter, the [Flake8](http://flake8.pycqa.org/en/latest/) code checker, and
the [isort](https://github.com/timothycrosley/isort) import orderer, so
certain code style requirements will be enforced when running automated tests.
See the Black and Flake8 documentation for more information.
See the Black, Flake8, and isort documentation for more information.
## Continuous integration
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/witten/borgmatic), and they're
also linked from the commits list on each pull request.
## Related documentation
* [Inspect your backups](../../docs/how-to/inspect-your-backups.md)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
+41 -2
View File
@@ -33,14 +33,53 @@ borgmatic --list
borgmatic --info
```
## Logging
By default, borgmatic logs to a local syslog-compatible daemon if one is
present. Where those logs show up depends on your particular system. If you're
using systemd, try running `journalctl -xe`. Otherwise, try viewing
`/var/log/syslog` or similiar.
You can customize the log level used for syslog logging with the
`--syslog-verbosity` flag, and this is independent from the console logging
`--verbosity` flag described above. For instance, to get additional
information about the progress of the backup as it proceeds:
```bash
borgmatic --syslog-verbosity 1
```
Or to increase syslog logging to include debug spew:
```bash
borgmatic --syslog-verbosity 2
```
### systemd journal
If your local syslog daemon is systemd's journal, be aware that journald by
default throttles the rate at which a particular program can log. So you may
need to [change the journald rate
limit](https://www.freedesktop.org/software/systemd/man/journald.conf.html#RateLimitIntervalSec=)
in `/etc/systemd/journald.conf` if you're finding that borgmatic journald logs
are missing.
Note that the [sample borgmatic systemd service
file](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#systemd)
already has this rate limit disabled.
## Scripting borgmatic
To consume the output of borgmatic in other software, you can include an
optional `--json` flag with `--create`, `--list`, or `--info` to get the
output formatted as JSON.
Note that when you specify the `--json` flag, Borg's other non-JSON output is
suppressed so as not to interfere with the captured JSON. Also note that JSON
output only shows up at the console, and not in syslog.
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [Develop on borgmatic](../../docs/how-to/develop-on-borgmatic.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
+5 -1
View File
@@ -105,7 +105,11 @@ include, the local file's option takes precedent. And note that this is a
shallow merge rather than a deep merge, so the merging does not descend into
nested values.
Note that this `<<` include merging syntax is only for merging in mappings
(keys/values). If you'd like to include other types like scalars or lists
directly, please see the section above about standard includes.
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
+2 -2
View File
@@ -57,5 +57,5 @@ Like a whole-archive restore, this also restores into the current directory.
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [Inspect your backups](../../docs/how-to/inspect-your-backups.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
@@ -36,6 +36,12 @@ hooks:
- echo "Error while creating a backup."
```
## Hook output
Any output produced by your hooks shows up both at the console and in syslog.
For more information, read about <a
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md">inspecting
your backups</a>.
## Security
@@ -47,5 +53,6 @@ injection or privilege escalation, do not forget to set secure permissions
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [Make per-application backups](../../docs/how-to/make-per-application-backups.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups.md)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
+58 -12
View File
@@ -7,16 +7,24 @@ To get up and running, first [install
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
least version 1.1.
Then, run the following command to download and install borgmatic:
Borgmatic consumes configurations in `/etc/borgmatic/` and `/etc/borgmatic.d/`
by default. Therefore, we show how to install borgmatic for the root user which
will have access permissions for these locations by default.
Run the following commands to download and install borgmatic:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
This is a [recommended user site
installation](https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site).
You will need to ensure that `/root/.local/bin` is available on your `$PATH` so
that the borgmatic executable is available.
Note that your pip binary may have a different name than "pip3". Make sure
you're using Python 3, as borgmatic does not support Python 2.
### Other ways to install
Along with the above process, you have several other options for installing
@@ -32,6 +40,18 @@ borgmatic:
* [openSUSE](https://software.opensuse.org/package/borgmatic)
## Hosting providers
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>
## Configuration
After you install borgmatic, generate a sample configuration file:
@@ -54,7 +74,7 @@ uncomment a particular option, also uncomment its containing section name, or
else borgmatic won't recognize the option.
You can also get the same sample configuration file from the [configuration
reference](../../docs/reference/configuration.md), the authoritative set of
reference](https://torsion.org/borgmatic/docs/reference/configuration.md), the authoritative set of
all configuration options. This is handy if borgmatic has added new options
since you originally created your configuration file.
@@ -76,7 +96,21 @@ FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-e
for more info.
###
### Validation
If you'd like to validate that your borgmatic configuration is valid, the
following command is available for that:
```bash
sudo validate-borgmatic-config
```
This command's exit status (`$?` in Bash) is zero when configuration is valid
and non-zero otherwise.
Validating configuration can be useful if you generate your configuration
files via configuration management, or you want to double check that your hand
edits are valid.
## Initialization
@@ -151,9 +185,9 @@ You can modify the cron file if you'd like to run borgmatic more or less frequen
If you're using systemd instead of cron to run jobs, download the [sample
systemd service
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.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/witten/borgmatic/src/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
@@ -165,6 +199,13 @@ sudo systemctl start borgmatic.timer
Feel free to modify the timer file based on how frequently you'd like
borgmatic to run.
## Colored output
Borgmatic produces colored terminal output by default. It is disabled when a
non-interactive terminal is detected (like a cron job). Otherwise, you can
disable it by passing the `--no-color` flag, setting the environment variable
`PY_COLORS=False`, or setting the `color` option to `false` in the `output`
section of configuration.
## Troubleshooting
@@ -181,8 +222,13 @@ it.
## Related documentation
* [Make per-application backups](../../docs/how-to/make-per-application-backups.md)
* [Deal with very large backups](../../docs/how-to/deal-with-very-large-backups.md)
* [Inspect your backups](../../docs/how-to/inspect-your-backups.md)
* [borgmatic configuration reference](../../docs/reference/configuration.md)
* [borgmatic command-line reference](../../docs/reference/command-line.md)
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups.md)
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups.md)
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration.md)
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line.md)
<script>
var links = document.getElementsByClassName("referral");
links[Math.floor(Math.random() * links.length)].style.display = "none";
</script>
+5 -5
View File
@@ -7,7 +7,7 @@ In general, all you should need to do to upgrade borgmatic is run the
following:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
See below about special cases.
@@ -25,14 +25,14 @@ already running borgmatic with Python 3, then you can upgrade borgmatic
in-place:
```bash
sudo pip3 install --upgrade borgmatic
sudo pip3 install --user --upgrade borgmatic
```
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
```bash
sudo pip uninstall borgmatic
sudo pip3 install borgmatic
sudo pip3 install --user borgmatic
```
The pip binary names for different versions of Python can differ, so the above
@@ -67,7 +67,7 @@ from atticmatic to borgmatic by running the following commands:
```bash
sudo pip3 uninstall atticmatic
sudo pip3 install borgmatic
sudo pip3 install --user borgmatic
```
That's it! borgmatic will continue using your /etc/borgmatic configuration
@@ -76,4 +76,4 @@ files.
## Related documentation
* [Develop on borgmatic](../../docs/how-to/develop-on-borgmatic.md)
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
+2 -2
View File
@@ -12,5 +12,5 @@ Here are all of the available borgmatic command-line options:
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [borgmatic configuration reference](../../docs/reference/configuration.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration.md)
+3 -3
View File
@@ -10,10 +10,10 @@ Here is a full sample borgmatic configuration file including all available optio
```
Note that you can also [download this configuration
file](../../docs/reference/config.yaml) for use locally.
file](https://torsion.org/borgmatic/docs/reference/config.yaml) for use locally.
## Related documentation
* [Set up backups with borgmatic](../../docs/how-to/set-up-backups.md)
* [borgmatic command-line reference](../../docs/reference/command-line.md)
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line.md)
+3
View File
@@ -0,0 +1,3 @@
[tool.black]
line-length = 100
skip-string-normalization = true
+1 -1
View File
@@ -1,3 +1,3 @@
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
0 3 * * * PATH=$PATH:/usr/bin /usr/bin/borgmatic
0 3 * * * PATH=$PATH:/usr/bin /root/.local/bin/borgmatic
+1 -1
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/local/bin /usr/local/bin/borgmatic
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
+2 -1
View File
@@ -3,4 +3,5 @@ Description=borgmatic backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/borgmatic
ExecStart=/root/.local/bin/borgmatic
LogRateLimitIntervalSec=0
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env sh
if which black; then
black --skip-string-normalization --line-length 100 --check .
else
echo "Skipping black due to not being installed."
fi
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -e
docker build --tag borgmatic-docs --file docs/Dockerfile .
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
# Temporary work around for https://github.com/pypa/pip/issues/6434
python -m pip install --upgrade pip==19.1.1
python -m pip install --no-use-pep517 $*
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# This script is intended to be run from the continuous integration build
# server, and not on a developer machine. For that, see:
# https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/
set -e
python -m pip install --upgrade pip==19.1.1
pip install tox==3.10.0
tox
apk add --no-cache borgbackup
tox -e end-to-end
+18
View File
@@ -1,2 +1,20 @@
[metadata]
description-file=README.md
[tool:pytest]
testpaths = tests
addopts = --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end
filterwarnings =
ignore:Coverage disabled.*:pytest.PytestWarning
[flake8]
ignore = E501,W503
exclude = *.*/*
[tool:isort]
force_single_line = False
include_trailing_comma = True
known_first_party = borgmatic
line_length = 100
multi_line_output = 3
skip = .tox
+10 -5
View File
@@ -1,13 +1,12 @@
from setuptools import setup, find_packages
from setuptools import find_packages, setup
VERSION = '1.3.1'
VERSION = '1.3.8'
setup(
name='borgmatic',
version=VERSION,
description='A wrapper script for Borg backup software that creates and prunes backups',
description='Simple, configuration-driven backup software for servers and workstations',
author='Dan Helfman',
author_email='witten@torsion.org',
url='https://torsion.org/borgmatic',
@@ -26,9 +25,15 @@ setup(
'borgmatic = borgmatic.commands.borgmatic:main',
'upgrade-borgmatic-config = borgmatic.commands.convert_config:main',
'generate-borgmatic-config = borgmatic.commands.generate_config:main',
'validate-borgmatic-config = borgmatic.commands.validate_config:main',
]
},
obsoletes=['atticmatic'],
install_requires=('pykwalify>=1.6.0,<14.06', 'ruamel.yaml>0.15.0,<0.16.0', 'setuptools'),
install_requires=(
'pykwalify>=1.6.0,<14.06',
'ruamel.yaml>0.15.0,<0.16.0',
'setuptools',
'colorama>=0.4.1,<0.5',
),
include_package_data=True,
)
+18 -17
View File
@@ -1,23 +1,24 @@
appdirs==1.4.3
atomicwrites==1.2.1
attrs==18.2.0
black==18.9b0; python_version >= '3.6'
Click==7.0
coverage==4.5.1
atomicwrites==1.3.0
attrs==19.1.0
black==19.3b0; python_version >= '3.6'
click==7.0
colorama==0.4.1
coverage==4.5.3
docopt==0.6.2
flake8==3.5.0
flexmock==0.10.2
flake8==3.7.7
flexmock==0.10.4
isort==4.3.20
mccabe==0.6.1
more-itertools==4.3.0
pluggy==0.7.1
py==1.6.0
pycodestyle==2.3.1
pyflakes==2.0.0
more-itertools==7.0.0
pluggy==0.12.0
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
pykwalify==1.7.0
pytest==3.8.2
pytest-cov==2.6.0
python-dateutil==2.7.3
PyYAML==3.13
pytest==4.6.3
pytest-cov==2.7.1
python-dateutil==2.8.0
PyYAML==5.1.1
ruamel.yaml>0.15.0,<0.16.0
six==1.11.0
toml==0.10.0
+36
View File
@@ -0,0 +1,36 @@
import os
import subprocess
import tempfile
def test_validate_config_command_with_valid_configuration_succeeds():
with tempfile.TemporaryDirectory() as temporary_directory:
config_path = os.path.join(temporary_directory, 'test.yaml')
subprocess.check_call(
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
)
exit_code = subprocess.call(
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
)
assert exit_code == 0
def test_validate_config_command_with_invalid_configuration_fails():
with tempfile.TemporaryDirectory() as temporary_directory:
config_path = os.path.join(temporary_directory, 'test.yaml')
subprocess.check_call(
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
)
config = open(config_path).read().replace('keep_daily: 7', 'keep_daily: "7"')
config_file = open(config_path, 'w')
config_file.write(config)
config_file.close()
exit_code = subprocess.call(
'validate-borgmatic-config --config {}'.format(config_path).split(' ')
)
assert exit_code == 1
+18 -3
View File
@@ -1,7 +1,7 @@
import subprocess
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.commands import borgmatic as module
@@ -14,7 +14,8 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
assert parser.config_paths == config_paths
assert parser.excludes_filename is None
assert parser.verbosity is 0
assert parser.verbosity == 0
assert parser.syslog_verbosity == 0
assert parser.json is False
@@ -24,7 +25,8 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
parser = module.parse_arguments('--config', 'myconfig', 'otherconfig')
assert parser.config_paths == ['myconfig', 'otherconfig']
assert parser.verbosity is 0
assert parser.verbosity == 0
assert parser.syslog_verbosity == 0
def test_parse_arguments_with_verbosity_overrides_default():
@@ -36,6 +38,19 @@ def test_parse_arguments_with_verbosity_overrides_default():
assert parser.config_paths == config_paths
assert parser.excludes_filename is None
assert parser.verbosity == 1
assert parser.syslog_verbosity == 0
def test_parse_arguments_with_syslog_verbosity_overrides_default():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
parser = module.parse_arguments('--syslog-verbosity', '2')
assert parser.config_paths == config_paths
assert parser.excludes_filename is None
assert parser.verbosity == 0
assert parser.syslog_verbosity == 2
def test_parse_arguments_with_json_overrides_default():
@@ -1,7 +1,7 @@
import os
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.commands import convert_config as module
@@ -0,0 +1,20 @@
from flexmock import flexmock
from borgmatic.commands import validate_config as module
def test_parse_arguments_with_no_arguments_uses_defaults():
config_paths = ['default']
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
parser = module.parse_arguments()
assert parser.config_paths == config_paths
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
parser = module.parse_arguments('--config', 'myconfig', 'otherconfig')
assert parser.config_paths == ['myconfig', 'otherconfig']
+2 -2
View File
@@ -1,9 +1,9 @@
from io import StringIO
import os
import sys
from io import StringIO
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import generate as module
+2 -3
View File
@@ -1,7 +1,6 @@
from io import StringIO
from collections import OrderedDict
import string
from collections import OrderedDict
from io import StringIO
from borgmatic.config import legacy as module
+22
View File
@@ -1,5 +1,7 @@
import sys
import pytest
import ruamel.yaml
from flexmock import flexmock
from borgmatic.config import load as module
@@ -38,3 +40,23 @@ def test_load_configuration_merges_include():
)
assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
def test_load_configuration_does_not_merge_include_list():
builtins = flexmock(sys.modules['builtins'])
builtins.should_receive('open').with_args('include.yaml').and_return(
'''
- one
- two
'''
)
builtins.should_receive('open').with_args('config.yaml').and_return(
'''
foo: bar
repositories:
<<: !include include.yaml
'''
)
with pytest.raises(ruamel.yaml.error.YAMLError):
assert module.load_configuration('config.yaml')
+1 -1
View File
@@ -2,8 +2,8 @@ import io
import string
import sys
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import validate as module
+36
View File
@@ -0,0 +1,36 @@
import logging
import subprocess
import pytest
from flexmock import flexmock
from borgmatic import execute as module
def test_execute_and_log_output_logs_each_line_separately():
flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'hi').once()
flexmock(module.logger).should_receive('log').with_args(logging.INFO, 'there').once()
module.execute_and_log_output(['echo', 'hi'], output_log_level=logging.INFO, shell=False)
module.execute_and_log_output(['echo', 'there'], output_log_level=logging.INFO, shell=False)
def test_execute_and_log_output_logs_borg_error_as_error():
flexmock(module.logger).should_receive('error').with_args('borg: error: oopsie').once()
module.execute_and_log_output(
['echo', 'borg: error: oopsie'], output_log_level=logging.INFO, shell=False
)
def test_execute_and_log_output_with_no_output_logs_nothing():
flexmock(module.logger).should_receive('log').never()
module.execute_and_log_output(['true'], output_log_level=logging.INFO, shell=False)
def test_execute_and_log_output_with_error_exit_status_raises():
flexmock(module.logger).should_receive('log').never()
with pytest.raises(subprocess.CalledProcessError):
module.execute_and_log_output(['false'], output_log_level=logging.INFO, shell=False)
+15 -40
View File
@@ -1,22 +1,19 @@
from subprocess import STDOUT
import logging
import sys
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.borg import check as module
from ..test_verbosity import insert_logging_mock
def insert_subprocess_mock(check_call_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
def insert_execute_command_mock(command):
flexmock(module).should_receive('execute_command').with_args(command).once()
def insert_subprocess_never():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').never()
def insert_execute_command_never():
flexmock(module).should_receive('execute_command').never()
def test_parse_checks_returns_them_as_tuple():
@@ -119,10 +116,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
flexmock(module).should_receive('_make_check_flags').with_args(
checks, check_last, None
).and_return(())
stdout = flexmock()
insert_subprocess_mock(('borg', 'check', 'repo'), stdout=stdout, stderr=STDOUT)
flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
insert_execute_command_mock(('borg', 'check', 'repo'))
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
@@ -136,7 +130,7 @@ def test_check_archives_with_extract_check_calls_extract_only():
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').never()
flexmock(module.extract).should_receive('extract_last_archive_dry_run').once()
insert_subprocess_never()
insert_execute_command_never()
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
@@ -149,7 +143,7 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_logging_mock(logging.INFO)
insert_subprocess_mock(('borg', 'check', 'repo', '--info'), stdout=None, stderr=STDOUT)
insert_execute_command_mock(('borg', 'check', 'repo', '--info'))
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
@@ -162,9 +156,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('_parse_checks').and_return(checks)
flexmock(module).should_receive('_make_check_flags').and_return(())
insert_logging_mock(logging.DEBUG)
insert_subprocess_mock(
('borg', 'check', 'repo', '--debug', '--show-rc'), stdout=None, stderr=STDOUT
)
insert_execute_command_mock(('borg', 'check', 'repo', '--debug', '--show-rc'))
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
@@ -174,7 +166,7 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
def test_check_archives_without_any_checks_bails():
consistency_config = {'check_last': None}
flexmock(module).should_receive('_parse_checks').and_return(())
insert_subprocess_never()
insert_execute_command_never()
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
@@ -189,10 +181,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('_make_check_flags').with_args(
checks, check_last, None
).and_return(())
stdout = flexmock()
insert_subprocess_mock(('borg1', 'check', 'repo'), stdout=stdout, stderr=STDOUT)
flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
insert_execute_command_mock(('borg1', 'check', 'repo'))
module.check_archives(
repository='repo',
@@ -210,12 +199,7 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
flexmock(module).should_receive('_make_check_flags').with_args(
checks, check_last, None
).and_return(())
stdout = flexmock()
insert_subprocess_mock(
('borg', 'check', 'repo', '--remote-path', 'borg1'), stdout=stdout, stderr=STDOUT
)
flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
insert_execute_command_mock(('borg', 'check', 'repo', '--remote-path', 'borg1'))
module.check_archives(
repository='repo',
@@ -233,12 +217,7 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
flexmock(module).should_receive('_make_check_flags').with_args(
checks, check_last, None
).and_return(())
stdout = flexmock()
insert_subprocess_mock(
('borg', 'check', 'repo', '--lock-wait', '5'), stdout=stdout, stderr=STDOUT
)
flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
insert_execute_command_mock(('borg', 'check', 'repo', '--lock-wait', '5'))
module.check_archives(
repository='repo', storage_config={'lock_wait': 5}, consistency_config=consistency_config
@@ -254,11 +233,7 @@ def test_check_archives_with_retention_prefix():
flexmock(module).should_receive('_make_check_flags').with_args(
checks, check_last, prefix
).and_return(())
stdout = flexmock()
insert_subprocess_mock(('borg', 'check', 'repo'), stdout=stdout, stderr=STDOUT)
flexmock(sys.modules['builtins']).should_receive('open').and_return(stdout)
flexmock(module.os).should_receive('devnull')
insert_execute_command_mock(('borg', 'check', 'repo'))
module.check_archives(
repository='repo', storage_config={}, consistency_config=consistency_config
+131 -28
View File
@@ -3,6 +3,7 @@ import logging
from flexmock import flexmock
from borgmatic.borg import create as module
from ..test_verbosity import insert_logging_mock
@@ -165,7 +166,7 @@ def test_create_archive_calls_borg_with_parameters():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND, capture_output=False
CREATE_COMMAND, output_log_level=logging.INFO
)
module.create_archive(
@@ -190,7 +191,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
flexmock(module).should_receive('_make_pattern_flags').and_return(pattern_flags)
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + pattern_flags, capture_output=False
CREATE_COMMAND + pattern_flags, output_log_level=logging.INFO
)
module.create_archive(
@@ -215,7 +216,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(exclude_flags)
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + exclude_flags, capture_output=False
CREATE_COMMAND + exclude_flags, output_log_level=logging.INFO
)
module.create_archive(
@@ -238,7 +239,8 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--stats'), capture_output=False
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--stats'),
output_log_level=logging.INFO,
)
insert_logging_mock(logging.INFO)
@@ -254,6 +256,31 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
)
def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--json',), output_log_level=None
)
insert_logging_mock(logging.INFO)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
json=True,
)
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
@@ -262,7 +289,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc'),
capture_output=False,
output_log_level=logging.INFO,
)
insert_logging_mock(logging.DEBUG)
@@ -278,6 +305,30 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
)
def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--json',), output_log_level=None
)
insert_logging_mock(logging.DEBUG)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
json=True,
)
def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
@@ -286,7 +337,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--dry-run',), capture_output=False
CREATE_COMMAND + ('--dry-run',), output_log_level=logging.INFO
)
module.create_archive(
@@ -311,7 +362,8 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--dry-run'), capture_output=False
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--dry-run'),
output_log_level=logging.INFO,
)
insert_logging_mock(logging.INFO)
@@ -338,7 +390,7 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run'),
capture_output=False,
output_log_level=logging.INFO,
)
insert_logging_mock(logging.DEBUG)
@@ -361,7 +413,7 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--checkpoint-interval', '600'), capture_output=False
CREATE_COMMAND + ('--checkpoint-interval', '600'), output_log_level=logging.INFO
)
module.create_archive(
@@ -383,7 +435,7 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--chunker-params', '1,2,3,4'), capture_output=False
CREATE_COMMAND + ('--chunker-params', '1,2,3,4'), output_log_level=logging.INFO
)
module.create_archive(
@@ -405,7 +457,7 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--compression', 'rle'), capture_output=False
CREATE_COMMAND + ('--compression', 'rle'), output_log_level=logging.INFO
)
module.create_archive(
@@ -427,7 +479,7 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--remote-ratelimit', '100'), capture_output=False
CREATE_COMMAND + ('--remote-ratelimit', '100'), output_log_level=logging.INFO
)
module.create_archive(
@@ -449,7 +501,7 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--one-file-system',), capture_output=False
CREATE_COMMAND + ('--one-file-system',), output_log_level=logging.INFO
)
module.create_archive(
@@ -472,7 +524,7 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_paramet
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--numeric-owner',), capture_output=False
CREATE_COMMAND + ('--numeric-owner',), output_log_level=logging.INFO
)
module.create_archive(
@@ -495,7 +547,7 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--read-special',), capture_output=False
CREATE_COMMAND + ('--read-special',), output_log_level=logging.INFO
)
module.create_archive(
@@ -518,7 +570,7 @@ def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parame
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND, capture_output=False
CREATE_COMMAND, output_log_level=logging.INFO
)
module.create_archive(
@@ -541,7 +593,7 @@ def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_paramete
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--nobsdflags',), capture_output=False
CREATE_COMMAND + ('--nobsdflags',), output_log_level=logging.INFO
)
module.create_archive(
@@ -564,7 +616,7 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--files-cache', 'ctime,size'), capture_output=False
CREATE_COMMAND + ('--files-cache', 'ctime,size'), output_log_level=logging.INFO
)
module.create_archive(
@@ -587,7 +639,7 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + CREATE_COMMAND[1:], capture_output=False
('borg1',) + CREATE_COMMAND[1:], output_log_level=logging.INFO
)
module.create_archive(
@@ -610,7 +662,7 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--remote-path', 'borg1'), capture_output=False
CREATE_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.INFO
)
module.create_archive(
@@ -633,7 +685,7 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--umask', '740'), capture_output=False
CREATE_COMMAND + ('--umask', '740'), output_log_level=logging.INFO
)
module.create_archive(
@@ -655,7 +707,7 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--lock-wait', '5'), capture_output=False
CREATE_COMMAND + ('--lock-wait', '5'), output_log_level=logging.INFO
)
module.create_archive(
@@ -670,6 +722,29 @@ def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
)
def test_create_archive_with_stats_calls_borg_with_stats_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--stats',), output_log_level=logging.WARNING
)
module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
stats=True,
)
def test_create_archive_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
@@ -677,7 +752,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--json',), capture_output=True
CREATE_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
json_output = module.create_archive(
@@ -695,6 +770,32 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
assert json_output == '[]'
def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
flexmock(module).should_receive('_write_pattern_file').and_return(None)
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
CREATE_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
json_output = module.create_archive(
dry_run=False,
repository='repo',
location_config={
'source_directories': ['foo', 'bar'],
'repositories': ['repo'],
'exclude_patterns': None,
},
storage_config={},
json=True,
stats=True,
)
assert json_output == '[]'
def test_create_archive_with_source_directories_glob_expands():
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
flexmock(module).should_receive('_expand_home_directories').and_return(())
@@ -703,7 +804,7 @@ def test_create_archive_with_source_directories_glob_expands():
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
capture_output=False,
output_log_level=logging.INFO,
)
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return(['foo', 'food'])
@@ -726,7 +827,8 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'), capture_output=False
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'),
output_log_level=logging.INFO,
)
flexmock(module.glob).should_receive('glob').with_args('foo*').and_return([])
@@ -750,7 +852,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
capture_output=False,
output_log_level=logging.INFO,
)
module.create_archive(
@@ -772,7 +874,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), capture_output=False
('borg', 'create', 'repo::ARCHIVE_NAME', 'foo', 'bar'), output_log_level=logging.INFO
)
module.create_archive(
@@ -794,7 +896,8 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
flexmock(module).should_receive('_make_pattern_flags').and_return(())
flexmock(module).should_receive('_make_exclude_flags').and_return(())
flexmock(module).should_receive('execute_command').with_args(
('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'), capture_output=False
('borg', 'create', 'repo::Documents_{hostname}-{now}', 'foo', 'bar'),
output_log_level=logging.INFO,
)
module.create_archive(
+2 -3
View File
@@ -42,8 +42,7 @@ def test_initialize_without_configuration_should_not_set_environment():
try:
os.environ = {}
module.initialize({})
assert os.environ.get('BORG_PASSCOMMAND') is None
assert os.environ.get('BORG_PASSPHRASE') is None
assert os.environ.get('BORG_RSH') is None
assert sum(1 for key in os.environ.keys() if key.startswith('BORG_')) == 0
finally:
os.environ = orig_environ
-25
View File
@@ -1,25 +0,0 @@
from flexmock import flexmock
from borgmatic.borg import execute as module
def test_execute_command_calls_full_command():
full_command = ['foo', 'bar']
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(full_command).once()
output = module.execute_command(full_command)
assert output is None
def test_execute_command_captures_output():
full_command = ['foo', 'bar']
expected_output = '[]'
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_output').with_args(full_command).and_return(
flexmock(decode=lambda: expected_output)
).once()
output = module.execute_command(full_command, capture_output=True)
assert output == expected_output
+40 -56
View File
@@ -1,67 +1,52 @@
import logging
import sys
from flexmock import flexmock
from borgmatic.borg import extract as module
from ..test_verbosity import insert_logging_mock
def insert_subprocess_mock(check_call_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
def insert_execute_command_mock(command):
flexmock(module).should_receive('execute_command').with_args(command).once()
def insert_subprocess_never():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').never()
def insert_subprocess_check_output_mock(check_output_command, result, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_output').with_args(check_output_command, **kwargs).and_return(
result
).once()
def insert_execute_command_output_mock(command, result):
flexmock(module).should_receive('execute_command').with_args(
command, output_log_level=None
).and_return(result).once()
def test_extract_last_archive_dry_run_calls_borg_with_last_archive():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'.encode('utf-8')
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo'), result='archive1\narchive2\n'
)
insert_subprocess_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2'))
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
def test_extract_last_archive_dry_run_without_any_archives_should_bail():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo'), result='\n'.encode('utf-8')
)
insert_subprocess_never()
def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
insert_execute_command_output_mock(('borg', 'list', '--short', 'repo'), result='\n')
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo', '--info'), result='archive1\narchive2\n'.encode('utf-8')
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo', '--info'), result='archive1\narchive2\n'
)
insert_subprocess_mock(('borg', 'extract', '--dry-run', 'repo::archive2', '--info'))
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2', '--info'))
insert_logging_mock(logging.INFO)
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo', '--debug', '--show-rc'),
result='archive1\narchive2\n'.encode('utf-8'),
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo', '--debug', '--show-rc'), result='archive1\narchive2\n'
)
insert_subprocess_mock(
insert_execute_command_mock(
('borg', 'extract', '--dry-run', 'repo::archive2', '--debug', '--show-rc', '--list')
)
insert_logging_mock(logging.DEBUG)
@@ -70,22 +55,19 @@ def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_param
def test_extract_last_archive_dry_run_calls_borg_via_local_path():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'.encode('utf-8')
insert_execute_command_output_mock(
('borg1', 'list', '--short', 'repo'), result='archive1\narchive2\n'
)
insert_subprocess_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
insert_execute_command_mock(('borg1', 'extract', '--dry-run', 'repo::archive2'))
module.extract_last_archive_dry_run(repository='repo', lock_wait=None, local_path='borg1')
def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'),
result='archive1\narchive2\n'.encode('utf-8'),
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'), result='archive1\narchive2\n'
)
insert_subprocess_mock(
insert_execute_command_mock(
('borg', 'extract', '--dry-run', 'repo::archive2', '--remote-path', 'borg1')
)
@@ -93,18 +75,18 @@ def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
def test_extract_last_archive_dry_run_calls_borg_with_lock_wait_parameters():
flexmock(sys.stdout).encoding = 'utf-8'
insert_subprocess_check_output_mock(
('borg', 'list', '--short', 'repo', '--lock-wait', '5'),
result='archive1\narchive2\n'.encode('utf-8'),
insert_execute_command_output_mock(
('borg', 'list', '--short', 'repo', '--lock-wait', '5'), result='archive1\narchive2\n'
)
insert_execute_command_mock(
('borg', 'extract', '--dry-run', 'repo::archive2', '--lock-wait', '5')
)
insert_subprocess_mock(('borg', 'extract', '--dry-run', 'repo::archive2', '--lock-wait', '5'))
module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
def test_extract_archive_calls_borg_with_restore_path_parameters():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', 'path1', 'path2'))
module.extract_archive(
dry_run=False,
@@ -117,7 +99,7 @@ def test_extract_archive_calls_borg_with_restore_path_parameters():
def test_extract_archive_calls_borg_with_remote_path_parameters():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--remote-path', 'borg1'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--remote-path', 'borg1'))
module.extract_archive(
dry_run=False,
@@ -131,7 +113,7 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
def test_extract_archive_calls_borg_with_numeric_owner_parameter():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--numeric-owner'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--numeric-owner'))
module.extract_archive(
dry_run=False,
@@ -144,7 +126,7 @@ def test_extract_archive_calls_borg_with_numeric_owner_parameter():
def test_extract_archive_calls_borg_with_umask_parameters():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--umask', '0770'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--umask', '0770'))
module.extract_archive(
dry_run=False,
@@ -157,7 +139,7 @@ def test_extract_archive_calls_borg_with_umask_parameters():
def test_extract_archive_calls_borg_with_lock_wait_parameters():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--lock-wait', '5'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--lock-wait', '5'))
module.extract_archive(
dry_run=False,
@@ -170,7 +152,7 @@ def test_extract_archive_calls_borg_with_lock_wait_parameters():
def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--info'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--info'))
insert_logging_mock(logging.INFO)
module.extract_archive(
@@ -184,7 +166,9 @@ def test_extract_archive_with_log_info_calls_borg_with_info_parameter():
def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--debug', '--list', '--show-rc'))
insert_execute_command_mock(
('borg', 'extract', 'repo::archive', '--debug', '--list', '--show-rc')
)
insert_logging_mock(logging.DEBUG)
module.extract_archive(
@@ -198,7 +182,7 @@ def test_extract_archive_with_log_debug_calls_borg_with_debug_parameters():
def test_extract_archive_calls_borg_with_dry_run_parameter():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--dry-run'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--dry-run'))
module.extract_archive(
dry_run=True,
@@ -211,7 +195,7 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
def test_extract_archive_calls_borg_with_progress_parameter():
insert_subprocess_mock(('borg', 'extract', 'repo::archive', '--progress'))
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--progress'))
module.extract_archive(
dry_run=False,
-22
View File
@@ -1,22 +0,0 @@
from flexmock import flexmock
from borgmatic.commands import hook as module
def test_execute_hook_invokes_each_command():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(':', shell=True).once()
module.execute_hook([':'], 'config.yaml', 'pre-backup')
def test_execute_hook_with_multiple_commands_invokes_each_command():
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(':', shell=True).once()
subprocess.should_receive('check_call').with_args('true', shell=True).once()
module.execute_hook([':', 'true'], 'config.yaml', 'pre-backup')
def test_execute_hook_with_empty_commands_does_not_raise():
module.execute_hook([], 'config.yaml', 'post-backup')
+32 -8
View File
@@ -3,38 +3,62 @@ import logging
from flexmock import flexmock
from borgmatic.borg import info as module
from ..test_verbosity import insert_logging_mock
from ..test_verbosity import insert_logging_mock
INFO_COMMAND = ('borg', 'info', 'repo')
def test_display_archives_info_calls_borg_with_parameters():
flexmock(module).should_receive('execute_command').with_args(INFO_COMMAND, capture_output=False)
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND, output_log_level=logging.WARNING
)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--info',), capture_output=False
INFO_COMMAND + ('--info',), output_log_level=logging.WARNING
)
insert_logging_mock(logging.INFO)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
insert_logging_mock(logging.INFO)
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
assert json_output == '[]'
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--debug', '--show-rc'), capture_output=False
INFO_COMMAND + ('--debug', '--show-rc'), output_log_level=logging.WARNING
)
insert_logging_mock(logging.DEBUG)
module.display_archives_info(repository='repo', storage_config={})
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
insert_logging_mock(logging.DEBUG)
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
assert json_output == '[]'
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--json',), capture_output=True
INFO_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
@@ -44,7 +68,7 @@ def test_display_archives_info_with_json_calls_borg_with_json_parameter():
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + INFO_COMMAND[1:], capture_output=False
('borg1',) + INFO_COMMAND[1:], output_log_level=logging.WARNING
)
module.display_archives_info(repository='repo', storage_config={}, local_path='borg1')
@@ -52,7 +76,7 @@ def test_display_archives_info_with_local_path_calls_borg_via_local_path():
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--remote-path', 'borg1'), capture_output=False
INFO_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
)
module.display_archives_info(repository='repo', storage_config={}, remote_path='borg1')
@@ -61,7 +85,7 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module).should_receive('execute_command').with_args(
INFO_COMMAND + ('--lock-wait', '5'), capture_output=False
INFO_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
)
module.display_archives_info(repository='repo', storage_config=storage_config)
+32 -16
View File
@@ -1,56 +1,72 @@
import logging
import subprocess
import pytest
from flexmock import flexmock
from borgmatic.borg import init as module
from ..test_verbosity import insert_logging_mock
INFO_REPOSITORY_EXISTS_RESPONSE_CODE = 0
INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE = 2
INFO_SOME_UNKNOWN_EXIT_CODE = -999
INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey')
def insert_info_command_mock(info_response):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('call').and_return(info_response)
def insert_info_command_found_mock():
flexmock(module).should_receive('execute_command')
def insert_info_command_not_found_mock():
flexmock(module).should_receive('execute_command').and_raise(
subprocess.CalledProcessError(module.INFO_REPOSITORY_NOT_FOUND_EXIT_CODE, [])
)
def insert_init_command_mock(init_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(init_command, **kwargs).once()
flexmock(module.subprocess).should_receive('check_call').with_args(
init_command, **kwargs
).once()
def test_initialize_repository_calls_borg_with_parameters():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND)
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_skips_initialization_when_repository_already_exists():
insert_info_command_mock(INFO_REPOSITORY_EXISTS_RESPONSE_CODE)
insert_info_command_found_mock()
flexmock(module.subprocess).should_receive('check_call').never()
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_raises_for_unknown_info_command_error():
flexmock(module).should_receive('execute_command').and_raise(
subprocess.CalledProcessError(INFO_SOME_UNKNOWN_EXIT_CODE, [])
)
with pytest.raises(subprocess.CalledProcessError):
module.initialize_repository(repository='repo', encryption_mode='repokey')
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--append-only',))
module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True)
def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G'))
module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G')
def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--info',))
insert_logging_mock(logging.INFO)
@@ -58,7 +74,7 @@ def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--debug',))
insert_logging_mock(logging.DEBUG)
@@ -66,14 +82,14 @@ def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
def test_initialize_repository_with_local_path_calls_borg_via_local_path():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:])
module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1')
def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter():
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
insert_info_command_not_found_mock()
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')
+29 -9
View File
@@ -3,40 +3,60 @@ import logging
from flexmock import flexmock
from borgmatic.borg import list as module
from ..test_verbosity import insert_logging_mock
from ..test_verbosity import insert_logging_mock
LIST_COMMAND = ('borg', 'list', 'repo')
def test_list_archives_calls_borg_with_parameters():
flexmock(module).should_receive('execute_command').with_args(LIST_COMMAND, capture_output=False)
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND, output_log_level=logging.WARNING
)
module.list_archives(repository='repo', storage_config={})
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--info',), capture_output=False
LIST_COMMAND + ('--info',), output_log_level=logging.WARNING
)
insert_logging_mock(logging.INFO)
module.list_archives(repository='repo', storage_config={})
def test_list_archives_with_log_info_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--json',), output_log_level=None
)
insert_logging_mock(logging.INFO)
module.list_archives(repository='repo', storage_config={}, json=True)
def test_list_archives_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--debug', '--show-rc'), capture_output=False
LIST_COMMAND + ('--debug', '--show-rc'), output_log_level=logging.WARNING
)
insert_logging_mock(logging.DEBUG)
module.list_archives(repository='repo', storage_config={})
def test_list_archives_with_log_debug_and_json_suppresses_most_borg_output():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--json',), output_log_level=None
)
insert_logging_mock(logging.DEBUG)
module.list_archives(repository='repo', storage_config={}, json=True)
def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
storage_config = {'lock_wait': 5}
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--lock-wait', '5'), capture_output=False
LIST_COMMAND + ('--lock-wait', '5'), output_log_level=logging.WARNING
)
module.list_archives(repository='repo', storage_config=storage_config)
@@ -45,7 +65,7 @@ def test_list_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
storage_config = {}
flexmock(module).should_receive('execute_command').with_args(
('borg', 'list', 'repo::archive'), capture_output=False
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING
)
module.list_archives(repository='repo', storage_config=storage_config, archive='archive')
@@ -53,7 +73,7 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
def test_list_archives_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('execute_command').with_args(
('borg1',) + LIST_COMMAND[1:], capture_output=False
('borg1',) + LIST_COMMAND[1:], output_log_level=logging.WARNING
)
module.list_archives(repository='repo', storage_config={}, local_path='borg1')
@@ -61,7 +81,7 @@ def test_list_archives_with_local_path_calls_borg_via_local_path():
def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--remote-path', 'borg1'), capture_output=False
LIST_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.WARNING
)
module.list_archives(repository='repo', storage_config={}, remote_path='borg1')
@@ -69,7 +89,7 @@ def test_list_archives_with_remote_path_calls_borg_with_remote_path_parameters()
def test_list_archives_with_json_calls_borg_with_json_parameter():
flexmock(module).should_receive('execute_command').with_args(
LIST_COMMAND + ('--json',), capture_output=True
LIST_COMMAND + ('--json',), output_log_level=None
).and_return('[]')
json_output = module.list_archives(repository='repo', storage_config={}, json=True)
+11 -11
View File
@@ -4,12 +4,12 @@ from collections import OrderedDict
from flexmock import flexmock
from borgmatic.borg import prune as module
from ..test_verbosity import insert_logging_mock
def insert_subprocess_mock(check_call_command, **kwargs):
subprocess = flexmock(module.subprocess)
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
def insert_execute_command_mock(prune_command, **kwargs):
flexmock(module).should_receive('execute_command').with_args(prune_command).once()
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
@@ -51,7 +51,7 @@ def test_prune_archives_calls_borg_with_parameters():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND)
insert_execute_command_mock(PRUNE_COMMAND)
module.prune_archives(
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
@@ -63,7 +63,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--stats', '--info'))
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info'))
insert_logging_mock(logging.INFO)
module.prune_archives(
@@ -76,7 +76,7 @@ def test_prune_archives_with_log_debug_calls_borg_with_debug_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc'))
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc'))
insert_logging_mock(logging.DEBUG)
module.prune_archives(
@@ -89,7 +89,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--dry-run',))
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run',))
module.prune_archives(
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
@@ -101,7 +101,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(('borg1',) + PRUNE_COMMAND[1:])
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:])
module.prune_archives(
dry_run=False,
@@ -117,7 +117,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
module.prune_archives(
dry_run=False,
@@ -134,7 +134,7 @@ def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--umask', '077'))
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077'))
module.prune_archives(
dry_run=False,
@@ -150,7 +150,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
BASE_PRUNE_FLAGS
)
insert_subprocess_mock(PRUNE_COMMAND + ('--lock-wait', '5'))
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5'))
module.prune_archives(
dry_run=False,
+42 -35
View File
@@ -3,99 +3,106 @@ from flexmock import flexmock
from borgmatic.commands import borgmatic as module
def test_load_configurations_collects_parsed_configurations():
configuration = flexmock()
other_configuration = flexmock()
flexmock(module.validate).should_receive('parse_configuration').and_return(
configuration
).and_return(other_configuration)
configs, logs = tuple(module.load_configurations(('test.yaml', 'other.yaml')))
assert configs == {'test.yaml': configuration, 'other.yaml': other_configuration}
assert logs == []
def test_load_configurations_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
configs, logs = tuple(module.load_configurations(('test.yaml',)))
assert configs == {}
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_info_for_success():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=True, list=False, repository='repo')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_critical_for_extract_with_repository_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
ValueError
)
args = flexmock(extract=True, list=False, repository='repo')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_and_repository_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
ValueError
)
args = flexmock(extract=False, list=True, repository='repo', archive='test')
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=True, repository='repo', archive=None)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert all(log for log in logs if log.levelno == module.logging.INFO)
def test_collect_configuration_run_summary_logs_critical_for_parse_error():
flexmock(module.validate).should_receive('parse_configuration').and_raise(ValueError)
args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_critical_for_run_error():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
args = flexmock(extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
flexmock(module.validate).should_receive('parse_configuration').and_return({'test.yaml': {}})
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(config_paths=(), extract=False, list=False)
logs = tuple(module.collect_configuration_run_summary_logs(config_filenames=(), args=args))
logs = tuple(module.collect_configuration_run_summary_logs({'test.yaml': {}}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
flexmock(module.validate).should_receive('parse_configuration').and_return(
{'test.yaml': {}, 'test2.yaml': {}}
)
flexmock(module).should_receive('run_configuration').and_return(['foo', 'bar']).and_return(
['baz']
)
flexmock(module.sys.stdout).should_receive('write').with_args('["foo", "bar", "baz"]').once()
args = flexmock(extract=False, list=False)
tuple(module.collect_configuration_run_summary_logs(('test.yaml', 'test2.yaml'), args=args))
tuple(
module.collect_configuration_run_summary_logs(
{'test.yaml': {}, 'test2.yaml': {}}, args=args
)
)
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
flexmock(module).should_receive('run_configuration').and_return([])
args = flexmock(extract=False, list=False, config_paths=[])
logs = tuple(module.collect_configuration_run_summary_logs({}, args=args))
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
+15
View File
@@ -30,6 +30,21 @@ def test_collect_config_filenames_collects_given_files():
assert config_filenames == config_paths
def test_collect_config_filenames_collects_yml_file_endings():
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)
mock_path.should_receive('isdir').with_args('/etc/borgmatic.d/foo.yml').and_return(False)
flexmock(module.os).should_receive('listdir')
flexmock(sys.modules['builtins']).should_receive('sorted').and_return(['foo.yml'])
config_filenames = tuple(module.collect_config_filenames(config_paths))
assert config_filenames == ('config.yaml', '/etc/borgmatic.d/foo.yml')
def test_collect_config_filenames_collects_files_from_given_directories_and_ignores_sub_directories():
config_paths = ('config.yaml', '/etc/borgmatic.d')
mock_path = flexmock(module.os.path)
+3 -4
View File
@@ -1,12 +1,11 @@
from collections import defaultdict, OrderedDict, namedtuple
import os
from collections import OrderedDict, defaultdict, namedtuple
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import convert as module
Parsed_config = namedtuple('Parsed_config', ('location', 'storage', 'retention', 'consistency'))
@@ -91,7 +90,7 @@ def test_guard_configuration_upgraded_raises_when_only_source_config_present():
flexmock(os.path).should_receive('exists').with_args('config.yaml').and_return(False)
flexmock(os.path).should_receive('exists').with_args('other.yaml').and_return(False)
with pytest.raises(module.LegacyConfigurationNotUpgraded):
with pytest.raises(module.Legacy_configuration_not_upgraded):
module.guard_configuration_upgraded('config', ('config.yaml', 'other.yaml'))
+1 -1
View File
@@ -1,7 +1,7 @@
from collections import OrderedDict
from flexmock import flexmock
import pytest
from flexmock import flexmock
from borgmatic.config import legacy as module
+1 -19
View File
@@ -1,5 +1,4 @@
import pytest
from flexmock import flexmock
from borgmatic.config import validate as module
@@ -37,20 +36,6 @@ def test_apply_logical_validation_raises_if_archive_name_format_present_without_
)
def test_apply_logical_validation_warns_if_archive_name_format_present_without_consistency_prefix():
logger = flexmock(module.logger)
logger.should_receive('warning').once()
module.apply_logical_validation(
'config.yaml',
{
'storage': {'archive_name_format': '{hostname}-{now}'},
'retention': {'prefix': '{hostname}-'},
'consistency': {},
},
)
def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
with pytest.raises(module.Validation_error):
module.apply_logical_validation(
@@ -74,10 +59,7 @@ def test_apply_locical_validation_does_not_raise_if_known_repository_in_check_re
)
def test_apply_logical_validation_does_not_raise_or_warn_if_archive_name_format_and_prefix_present():
logger = flexmock(module.logger)
logger.should_receive('warning').never()
def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_prefix_present():
module.apply_logical_validation(
'config.yaml',
{
+51
View File
@@ -0,0 +1,51 @@
import logging
from flexmock import flexmock
from borgmatic import execute as module
def test_execute_command_calls_full_command():
full_command = ['foo', 'bar']
flexmock(module).should_receive('execute_and_log_output').with_args(
full_command, output_log_level=logging.INFO, shell=False
).once()
output = module.execute_command(full_command)
assert output is None
def test_execute_command_calls_full_command_with_shell():
full_command = ['foo', 'bar']
flexmock(module).should_receive('execute_and_log_output').with_args(
full_command, output_log_level=logging.INFO, shell=True
).once()
output = module.execute_command(full_command, shell=True)
assert output is None
def test_execute_command_captures_output():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, shell=False
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command(full_command, output_log_level=None)
assert output == expected_output
def test_execute_command_captures_output_with_shell():
full_command = ['foo', 'bar']
expected_output = '[]'
flexmock(module.subprocess).should_receive('check_output').with_args(
full_command, shell=True
).and_return(flexmock(decode=lambda: expected_output)).once()
output = module.execute_command(full_command, output_log_level=None, shell=True)
assert output == expected_output
+52
View File
@@ -0,0 +1,52 @@
import logging
from flexmock import flexmock
from borgmatic import hook as module
def test_execute_hook_invokes_each_command():
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
).once()
module.execute_hook([':'], None, 'config.yaml', 'pre-backup', dry_run=False)
def test_execute_hook_with_multiple_commands_invokes_each_command():
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
).once()
flexmock(module.execute).should_receive('execute_command').with_args(
['true'], output_log_level=logging.WARNING, shell=True
).once()
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=False)
def test_execute_hook_with_umask_sets_that_umask():
flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
flexmock(module.os).should_receive('umask').with_args(0o22).once()
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.WARNING, shell=True
)
module.execute_hook([':'], 77, 'config.yaml', 'pre-backup', dry_run=False)
def test_execute_hook_with_dry_run_skips_commands():
flexmock(module.execute).should_receive('execute_command').never()
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
def test_execute_hook_with_empty_commands_does_not_raise():
module.execute_hook([], None, 'config.yaml', 'post-backup', dry_run=False)
def test_execute_hook_on_error_logs_as_error():
flexmock(module.execute).should_receive('execute_command').with_args(
[':'], output_log_level=logging.ERROR, shell=True
).once()
module.execute_hook([':'], None, 'config.yaml', 'on-error', dry_run=False)
+157
View File
@@ -0,0 +1,157 @@
import logging
import pytest
from flexmock import flexmock
from borgmatic import logger as module
@pytest.mark.parametrize('bool_val', (True, 'yes', 'on', '1', 'true', 'True', 1))
def test_to_bool_parses_true_values(bool_val):
assert module.to_bool(bool_val)
@pytest.mark.parametrize('bool_val', (False, 'no', 'off', '0', 'false', 'False', 0))
def test_to_bool_parses_false_values(bool_val):
assert not module.to_bool(bool_val)
def test_to_bool_passes_none_through():
assert module.to_bool(None) is None
def test_should_do_markup_respects_no_color_value():
assert module.should_do_markup(no_color=True, configs={}) is False
def test_should_do_markup_respects_config_value():
assert (
module.should_do_markup(no_color=False, configs={'foo.yaml': {'output': {'color': False}}})
is False
)
def test_should_do_markup_prefers_any_false_config_value():
assert (
module.should_do_markup(
no_color=False,
configs={
'foo.yaml': {'output': {'color': True}},
'bar.yaml': {'output': {'color': False}},
},
)
is False
)
def test_should_do_markup_respects_PY_COLORS_environment_variable():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False, configs={}) is True
def test_should_do_markup_prefers_no_color_value_to_config_value():
assert (
module.should_do_markup(no_color=True, configs={'foo.yaml': {'output': {'color': True}}})
is False
)
def test_should_do_markup_prefers_config_value_to_PY_COLORS():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert (
module.should_do_markup(no_color=False, configs={'foo.yaml': {'output': {'color': False}}})
is False
)
def test_should_do_markup_prefers_no_color_value_to_PY_COLORS():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=True, configs={}) is False
def test_should_do_markup_respects_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
assert module.should_do_markup(no_color=False, configs={}) is False
def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value():
flexmock(module.os.environ).should_receive('get').and_return('True')
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False, configs={}) is True
def test_console_color_formatter_format_includes_log_message():
plain_message = 'uh oh'
record = flexmock(levelno=logging.CRITICAL, msg=plain_message)
colored_message = module.Console_color_formatter().format(record)
assert colored_message != plain_message
assert plain_message in colored_message
def test_color_text_does_not_raise():
module.color_text(module.colorama.Fore.RED, 'hi')
def test_color_text_without_color_does_not_raise():
module.color_text(None, 'hi')
def test_configure_logging_probes_for_log_socket_on_linux():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple
)
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(False)
syslog_handler = logging.handlers.SysLogHandler()
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
address='/dev/log'
).and_return(syslog_handler).once()
module.configure_logging(logging.INFO)
def test_configure_logging_probes_for_log_socket_on_macos():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple
)
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(False)
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(True)
syslog_handler = logging.handlers.SysLogHandler()
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
address='/var/run/syslog'
).and_return(syslog_handler).once()
module.configure_logging(logging.INFO)
def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.DEBUG, handlers=tuple
).once()
flexmock(module.os.path).should_receive('exists').and_return(False)
module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DEBUG)
def test_configure_logging_skips_syslog_if_not_found():
flexmock(module).should_receive('Console_color_formatter')
flexmock(module.logging).should_receive('basicConfig').with_args(
level=logging.INFO, handlers=tuple
)
flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
module.configure_logging(console_log_level=logging.INFO)
+24 -11
View File
@@ -1,27 +1,40 @@
[tox]
envlist = py3
envlist = py35,py36,py37
skip_missing_interpreters = True
skipsdist = True
minversion = 3.10.0
[testenv]
usedevelop = True
deps = -rtest_requirements.txt
whitelist_externals = sh
whitelist_externals =
find
sh
install_command =
sh scripts/pip {opts} {packages}
commands_pre =
find {toxinidir} -type f -not -path '{toxinidir}/.tox/*' -path '*/__pycache__/*' -name '*.py[c|o]' -delete
commands =
py.test --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end \
tests []
sh scripts/black
flake8 .
pytest {posargs}
py36,py37: black --check .
isort --recursive --check-only --settings-path setup.cfg .
flake8 borgmatic tests
[testenv:black]
basepython = python3.7
commands =
black --skip-string-normalization --line-length 100 .
black {posargs} .
[testenv:test]
commands =
pytest {posargs}
[testenv:end-to-end]
deps = -rtest_requirements.txt
commands =
py.test tests/end-to-end []
pytest {posargs} --no-cov tests/end-to-end
[flake8]
ignore = E501,W503
exclude = *.*/*
[testenv:isort]
deps = {[testenv]deps}
commands =
isort {posargs:--recursive} --settings-path setup.cfg .