Compare commits
82 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e3c2da79c | |||
| 37dc94bc79 | |||
| fc274b43f0 | |||
| 9ab12e4312 | |||
| a5ff35c198 | |||
| 458e7776c5 | |||
| fa5fa1c11b | |||
| f8bc67be8d | |||
| 17586d49ac | |||
| 2f75c9aa9e | |||
|
60650ccfc7 |
|||
| c12c47cace | |||
| d6aaab8a09 | |||
| 128ebf04ce | |||
| b1941bcce9 | |||
| 7b3b28616d | |||
| f3910f49ca | |||
| 59e1cac92c | |||
| b1f0287fdb | |||
| 99c35d4077 | |||
| 07b9ff61f2 | |||
| f573c1810a | |||
| 1d37b14356 | |||
| 6c617eddd5 | |||
| e14ebee4e0 | |||
| a897ffd514 | |||
| a472735616 | |||
| b3fec03cf4 | |||
| 89dccc25c3 | |||
| 3846155d62 | |||
| 386979ebb4 | |||
| 07222cd984 | |||
| cf4c6c274d | |||
| 340bd72176 | |||
| 1a1bb71af1 | |||
| ae45dfe63a | |||
| d6ac7a9192 | |||
| d959fdbf8d | |||
| 81739791e0 | |||
| 4cdff74e9b | |||
| 11e830bb1d | |||
| cba00a9c4e | |||
| f2198de151 | |||
|
0c439c0c02 |
|||
| f11a9bb4aa | |||
| ee6f390910 | |||
|
9a5117db14 |
|||
| 9585c8f908 | |||
| 3495484ddd | |||
| 67ab2acb82 | |||
| c085bacccf | |||
| 896401088e | |||
| ef3dda9213 | |||
| c9f5d9b048 | |||
| ccbd0b608b | |||
| a7cc2ea803 | |||
| 9ec75ccf3f | |||
| 7c890be76d | |||
| 39e5aac479 | |||
| e25f2c4e6c | |||
| 7ad8f9ac6f | |||
| 2add3ff7ad | |||
| 0602ca1862 | |||
| e973802fc1 | |||
| 2bdf6dfd70 | |||
| f894c49540 | |||
| 7900e5ea53 | |||
| 5587f48bda | |||
| de3ee07566 | |||
| fe39453598 | |||
| 9c75063c05 | |||
| 5cf2ef1732 | |||
| f35e6ea7ad | |||
| 90595e9c18 | |||
| 032d4adee3 | |||
| 4444219e17 | |||
| 56fd78089d | |||
| 86dbc00cbe | |||
| c644270599 | |||
| 1676a98c51 | |||
| 358ed53da0 | |||
| 90925c9428 |
77 changed files with 2948 additions and 675 deletions
12
.drone.yml
12
.drone.yml
|
|
@ -1,30 +1,30 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: python-3-5-alpine-3-9
|
||||
name: python-3-5-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.5-alpine3.9
|
||||
image: python:3.5-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-6-alpine-3-9
|
||||
name: python-3-6-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.6-alpine3.9
|
||||
image: python:3.6-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
---
|
||||
kind: pipeline
|
||||
name: python-3-7-alpine-3-9
|
||||
name: python-3-7-alpine-3-10
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: python:3.7-alpine3.9
|
||||
image: python:3.7-alpine3.10
|
||||
pull: always
|
||||
commands:
|
||||
- scripts/run-tests
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ module.exports = function(eleventyConfig) {
|
|||
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$/, '/');
|
||||
if (process.env.NODE_ENV == "production") {
|
||||
return link;
|
||||
}
|
||||
return link.replace('https://torsion.org/borgmatic/', 'http://localhost:8080/');
|
||||
}
|
||||
};
|
||||
let markdownItAnchorOptions = {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,6 @@ Use `sudo borg --version`
|
|||
|
||||
**Python version:** [version here]
|
||||
|
||||
Use `python --version`
|
||||
Use `python3 --version`
|
||||
|
||||
**operating system and version:** [OS here]
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@
|
|||
.coverage
|
||||
.pytest_cache
|
||||
.tox
|
||||
__pycache__
|
||||
build/
|
||||
dist/
|
||||
pip-wheel-metadata/
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
include borgmatic/config/schema.yaml
|
||||
graft sample/systemd
|
||||
|
|
|
|||
89
NEWS
89
NEWS
|
|
@ -1,3 +1,92 @@
|
|||
1.4.0
|
||||
* #225: Database dump hooks for PostgreSQL, so you can easily dump your databases before backups
|
||||
run.
|
||||
* #230: Rename "borgmatic list --pattern-from" flag to "--patterns-from" to match Borg.
|
||||
|
||||
1.3.26
|
||||
* #224: Fix "borgmatic list --successful" with a slightly better heuristic for listing successful
|
||||
(non-checkpoint) archives.
|
||||
|
||||
1.3.25
|
||||
* #223: Dead man's switch to detect when backups start failing silently, implemented via
|
||||
healthchecks.io hook integration. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook
|
||||
* Documentation on monitoring and alerting options for borgmatic backups:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/
|
||||
* Automatically rewrite links when developing on documentation locally.
|
||||
|
||||
1.3.24
|
||||
* #86: Add "borgmatic list --successful" flag to only list successful (non-checkpoint) archives.
|
||||
* Add a suggestion form to all documentation pages, so users can submit ideas for improving the
|
||||
documentation.
|
||||
* Update documentation link to community Arch Linux borgmatic package.
|
||||
|
||||
1.3.23
|
||||
* #174: More detailed error alerting via runtime context available in "on_error" hook.
|
||||
|
||||
1.3.22
|
||||
* #144: When backups to one of several repositories fails, keep backing up to the other
|
||||
repositories and report errors afterwards.
|
||||
|
||||
1.3.21
|
||||
* #192: User-defined hooks for global setup or cleanup that run before/after all actions. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/
|
||||
|
||||
1.3.20
|
||||
* #205: More robust sample systemd service: boot delay, network dependency, lowered CPU/IO
|
||||
priority, etc.
|
||||
* #221: Fix "borgmatic create --progress" output so that it updates on the console in real-time.
|
||||
|
||||
1.3.19
|
||||
* #219: Fix visibility of "borgmatic prune --stats" output.
|
||||
|
||||
1.3.18
|
||||
* #220: Fix regression of argument parsing for default actions.
|
||||
|
||||
1.3.17
|
||||
* #217: Fix error with "borgmatic check --only" command-line flag with "extract" consistency check.
|
||||
|
||||
1.3.16
|
||||
* #210: Support for Borg check --verify-data flag via borgmatic "data" consistency check.
|
||||
* #210: Override configured consistency checks via "borgmatic check --only" command-line flag.
|
||||
* When generating sample configuration with generate-borgmatic-config, add a space after each "#"
|
||||
comment indicator.
|
||||
|
||||
1.3.15
|
||||
* #208: Fix for traceback when the "checks" option has an empty value.
|
||||
* #209: Bypass Borg error about a moved repository via "relocated_repo_access_is_ok" option in
|
||||
borgmatic storage configuration section.
|
||||
* #213: Reorder arguments passed to Borg to fix duplicate directories when using Borg patterns.
|
||||
* #214: Fix for hook erroring with exit code 1 not being interpreted as an error.
|
||||
|
||||
1.3.14
|
||||
* #204: Do not treat Borg warnings (exit code 1) as failures.
|
||||
* When validating configuration files, require strings instead of allowing any scalar type.
|
||||
|
||||
1.3.13
|
||||
* #199: Add note to documentation about using spaces instead of tabs for indentation, as YAML does
|
||||
not allow tabs.
|
||||
* #203: Fix compatibility with ruamel.yaml 0.16.x.
|
||||
* If a "prefix" option in borgmatic's configuration has an empty value (blank or ""), then disable
|
||||
default prefix.
|
||||
|
||||
1.3.12
|
||||
* Only log to syslog when run from a non-interactive console (e.g. a cron job).
|
||||
* Remove unicode byte order mark from syslog output so it doesn't show up as a literal in rsyslog
|
||||
output. See discussion on #197.
|
||||
|
||||
1.3.11
|
||||
* #193: Pass through several "borg list" and "borg info" flags like --short, --format, --sort-by,
|
||||
--first, --last, etc. via borgmatic command-line flags.
|
||||
* Add borgmatic info --repository and --archive command-line flags to display info for individual
|
||||
repositories or archives.
|
||||
* Support for Borg --noatime, --noctime, and --nobirthtime flags via corresponding options in
|
||||
borgmatic configuration location section.
|
||||
|
||||
1.3.10
|
||||
* #198: Fix for Borg create error output not showing up at borgmatic verbosity level zero.
|
||||
|
||||
1.3.9
|
||||
* #195: Switch to command-line actions as more traditional sub-commands, e.g. "borgmatic create",
|
||||
"borgmatic prune", etc. However, the classic dashed options like "--create" still work!
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -2,7 +2,7 @@
|
|||
title: borgmatic
|
||||
permalink: index.html
|
||||
---
|
||||
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
||||
<a href="https://build.torsion.org/witten/borgmatic" alt="build status"></a>
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
@ -41,10 +41,18 @@ retention:
|
|||
keep_monthly: 6
|
||||
|
||||
consistency:
|
||||
# List of consistency checks to run: "repository", "archives", or both.
|
||||
# List of consistency checks to run: "repository", "archives", etc.
|
||||
checks:
|
||||
- repository
|
||||
- archives
|
||||
|
||||
hooks:
|
||||
# Preparation scripts to run, databases to dump, and monitoring to perform.
|
||||
before_backup:
|
||||
- prepare-for-backup.sh
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
|
||||
```
|
||||
|
||||
borgmatic is hosted at <https://torsion.org/borgmatic> with [source code
|
||||
|
|
@ -63,8 +71,10 @@ href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
|
|||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [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/)
|
||||
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Upgrade borgmatic](https://torsion.org/borgmatic/docs/how-to/upgrade/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
||||
|
|
@ -116,8 +126,3 @@ your thing. In general, contributions are very welcome. We don't bite!
|
|||
Also, please check out the [borgmatic development
|
||||
how-to](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/) for
|
||||
info on cloning source code, running tests, etc.
|
||||
|
||||
<script>
|
||||
var links = document.getElementsByClassName("referral");
|
||||
links[Math.floor(Math.random() * links.length)].style.display = "none";
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ DEFAULT_PREFIX = '{hostname}-'
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_checks(consistency_config):
|
||||
def _parse_checks(consistency_config, only_checks=None):
|
||||
'''
|
||||
Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
|
||||
Given a consistency config with a "checks" list, and an optional list of override checks,
|
||||
transform them a tuple of named checks to run.
|
||||
|
||||
For example, given a retention config of:
|
||||
|
||||
|
|
@ -22,16 +23,21 @@ def _parse_checks(consistency_config):
|
|||
|
||||
('repository', 'archives')
|
||||
|
||||
If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
|
||||
"disabled", return an empty tuple, meaning that no checks should be run.
|
||||
If no "checks" option is present in the config, return the DEFAULT_CHECKS. If the checks value
|
||||
is the string "disabled", return an empty tuple, meaning that no checks should be run.
|
||||
|
||||
If the "data" option is present, then make sure the "archives" option is included as well.
|
||||
'''
|
||||
checks = consistency_config.get('checks', [])
|
||||
checks = [
|
||||
check.lower() for check in (only_checks or consistency_config.get('checks', []) or [])
|
||||
]
|
||||
if checks == ['disabled']:
|
||||
return ()
|
||||
|
||||
return (
|
||||
tuple(check for check in checks if check.lower() not in ('disabled', '')) or DEFAULT_CHECKS
|
||||
)
|
||||
if 'data' in checks and 'archives' not in checks:
|
||||
checks.append('archives')
|
||||
|
||||
return tuple(check for check in checks if check not in ('disabled', '')) or DEFAULT_CHECKS
|
||||
|
||||
|
||||
def _make_check_flags(checks, check_last=None, prefix=None):
|
||||
|
|
@ -55,7 +61,7 @@ def _make_check_flags(checks, check_last=None, prefix=None):
|
|||
'''
|
||||
if 'archives' in checks:
|
||||
last_flags = ('--last', str(check_last)) if check_last else ()
|
||||
prefix_flags = ('--prefix', prefix) if prefix else ('--prefix', DEFAULT_PREFIX)
|
||||
prefix_flags = ('--prefix', prefix) if prefix else ()
|
||||
else:
|
||||
last_flags = ()
|
||||
prefix_flags = ()
|
||||
|
|
@ -68,30 +74,37 @@ def _make_check_flags(checks, check_last=None, prefix=None):
|
|||
'Ignoring consistency prefix option, as "archives" is not in consistency checks.'
|
||||
)
|
||||
|
||||
common_flags = last_flags + prefix_flags + (('--verify-data',) if 'data' in checks else ())
|
||||
|
||||
if set(DEFAULT_CHECKS).issubset(set(checks)):
|
||||
return last_flags + prefix_flags
|
||||
return common_flags
|
||||
|
||||
return (
|
||||
tuple('--{}-only'.format(check) for check in checks if check in DEFAULT_CHECKS)
|
||||
+ last_flags
|
||||
+ prefix_flags
|
||||
+ common_flags
|
||||
)
|
||||
|
||||
|
||||
def check_archives(
|
||||
repository, storage_config, consistency_config, local_path='borg', remote_path=None
|
||||
repository,
|
||||
storage_config,
|
||||
consistency_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
only_checks=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a storage config dict, a consistency config dict,
|
||||
and a local/remote commands to run, check the contained Borg archives for consistency.
|
||||
local/remote commands to run, and an optional list of checks to use instead of configured
|
||||
checks, check the contained Borg archives for consistency.
|
||||
|
||||
If there are no consistency checks to run, skip running them.
|
||||
'''
|
||||
checks = _parse_checks(consistency_config)
|
||||
checks = _parse_checks(consistency_config, only_checks)
|
||||
check_last = consistency_config.get('check_last', None)
|
||||
lock_wait = None
|
||||
|
||||
if set(checks).intersection(set(DEFAULT_CHECKS)):
|
||||
if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))):
|
||||
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
|
||||
|
|
@ -102,14 +115,15 @@ def check_archives(
|
|||
if logger.isEnabledFor(logging.DEBUG):
|
||||
verbosity_flags = ('--debug', '--show-rc')
|
||||
|
||||
prefix = consistency_config.get('prefix')
|
||||
prefix = consistency_config.get('prefix', DEFAULT_PREFIX)
|
||||
|
||||
full_command = (
|
||||
(local_path, 'check', repository)
|
||||
(local_path, 'check')
|
||||
+ _make_check_flags(checks, check_last, prefix)
|
||||
+ remote_path_flags
|
||||
+ lock_wait_flags
|
||||
+ verbosity_flags
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -60,8 +60,8 @@ def _write_pattern_file(patterns=None):
|
|||
|
||||
def _make_pattern_flags(location_config, pattern_filename=None):
|
||||
'''
|
||||
Given a location config dict with a potential pattern_from option, and a filename containing any
|
||||
additional patterns, return the corresponding Borg flags for those files as a tuple.
|
||||
Given a location config dict with a potential patterns_from option, and a filename containing
|
||||
any additional patterns, return the corresponding Borg flags for those files as a tuple.
|
||||
'''
|
||||
pattern_filenames = tuple(location_config.get('patterns_from') or ()) + (
|
||||
(pattern_filename,) if pattern_filename else ()
|
||||
|
|
@ -94,6 +94,20 @@ def _make_exclude_flags(location_config, exclude_filename=None):
|
|||
return exclude_from_flags + caches_flag + if_present_flags
|
||||
|
||||
|
||||
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
|
||||
|
||||
|
||||
def borgmatic_source_directories():
|
||||
'''
|
||||
Return a list of borgmatic-specific source directories used for state like database backups.
|
||||
'''
|
||||
return (
|
||||
[BORGMATIC_SOURCE_DIRECTORY]
|
||||
if os.path.exists(os.path.expanduser(BORGMATIC_SOURCE_DIRECTORY))
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
def create_archive(
|
||||
dry_run,
|
||||
repository,
|
||||
|
|
@ -109,7 +123,9 @@ def create_archive(
|
|||
Given vebosity/dry-run flags, a local or remote repository path, a location config dict, and a
|
||||
storage config dict, create a Borg archive and return Borg's JSON output (if any).
|
||||
'''
|
||||
sources = _expand_directories(location_config['source_directories'])
|
||||
sources = _expand_directories(
|
||||
location_config['source_directories'] + borgmatic_source_directories()
|
||||
)
|
||||
|
||||
pattern_file = _write_pattern_file(location_config.get('patterns'))
|
||||
exclude_file = _write_pattern_file(
|
||||
|
|
@ -126,14 +142,7 @@ def create_archive(
|
|||
archive_name_format = storage_config.get('archive_name_format', default_archive_name_format)
|
||||
|
||||
full_command = (
|
||||
(
|
||||
local_path,
|
||||
'create',
|
||||
'{repository}::{archive_name_format}'.format(
|
||||
repository=repository, archive_name_format=archive_name_format
|
||||
),
|
||||
)
|
||||
+ sources
|
||||
(local_path, 'create')
|
||||
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
||||
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
||||
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
||||
|
|
@ -142,6 +151,9 @@ def create_archive(
|
|||
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
|
||||
+ (('--one-file-system',) if location_config.get('one_file_system') else ())
|
||||
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
|
||||
+ (('--noatime',) if location_config.get('atime') is False else ())
|
||||
+ (('--noctime',) if location_config.get('ctime') is False else ())
|
||||
+ (('--nobirthtime',) if location_config.get('birthtime') is False else ())
|
||||
+ (('--read-special',) if location_config.get('read_special') else ())
|
||||
+ (('--nobsdflags',) if location_config.get('bsd_flags') is False else ())
|
||||
+ (('--files-cache', files_cache) if files_cache else ())
|
||||
|
|
@ -159,8 +171,20 @@ def create_archive(
|
|||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (('--json',) if json else ())
|
||||
+ (
|
||||
'{repository}::{archive_name_format}'.format(
|
||||
repository=repository, archive_name_format=archive_name_format
|
||||
),
|
||||
)
|
||||
+ sources
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
return
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif stats:
|
||||
|
|
|
|||
|
|
@ -11,9 +11,21 @@ OPTION_TO_ENVIRONMENT_VARIABLE = {
|
|||
'ssh_command': 'BORG_RSH',
|
||||
}
|
||||
|
||||
DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE = {
|
||||
'relocated_repo_access_is_ok': 'BORG_RELOCATED_REPO_ACCESS_IS_OK',
|
||||
'unknown_unencrypted_repo_access_is_ok': 'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK',
|
||||
}
|
||||
|
||||
|
||||
def initialize(storage_config):
|
||||
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
|
||||
|
||||
for (
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_ENVIRONMENT_VARIABLE.items():
|
||||
value = storage_config.get(option_name, False)
|
||||
os.environ[environment_variable_name] = 'yes' if value else 'no'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -19,10 +19,11 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
|
|||
verbosity_flags = ('--info',)
|
||||
|
||||
full_list_command = (
|
||||
(local_path, 'list', '--short', repository)
|
||||
(local_path, 'list', '--short')
|
||||
+ remote_path_flags
|
||||
+ lock_wait_flags
|
||||
+ verbosity_flags
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
list_output = execute_command(full_list_command, output_log_level=None)
|
||||
|
|
@ -34,18 +35,16 @@ def extract_last_archive_dry_run(repository, lock_wait=None, local_path='borg',
|
|||
|
||||
list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
|
||||
full_extract_command = (
|
||||
(
|
||||
local_path,
|
||||
'extract',
|
||||
'--dry-run',
|
||||
'{repository}::{last_archive_name}'.format(
|
||||
repository=repository, last_archive_name=last_archive_name
|
||||
),
|
||||
)
|
||||
(local_path, 'extract', '--dry-run')
|
||||
+ remote_path_flags
|
||||
+ lock_wait_flags
|
||||
+ verbosity_flags
|
||||
+ list_flag
|
||||
+ (
|
||||
'{repository}::{last_archive_name}'.format(
|
||||
repository=repository, last_archive_name=last_archive_name
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
execute_command(full_extract_command)
|
||||
|
|
@ -71,8 +70,7 @@ def extract_archive(
|
|||
lock_wait = storage_config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
(local_path, 'extract', '::'.join((repository, archive)))
|
||||
+ (tuple(restore_paths) if restore_paths else ())
|
||||
(local_path, 'extract')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--numeric-owner',) if location_config.get('numeric_owner') else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
|
|
@ -81,6 +79,14 @@ def extract_archive(
|
|||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ ('::'.join((repository, archive)),)
|
||||
+ (tuple(restore_paths) if restore_paths else ())
|
||||
)
|
||||
|
||||
# The progress output isn't compatible with captured and logged output, as progress messes with
|
||||
# the terminal directly.
|
||||
if progress:
|
||||
execute_command_without_capture(full_command)
|
||||
return
|
||||
|
||||
execute_command(full_command)
|
||||
|
|
|
|||
31
borgmatic/borg/flags.py
Normal file
31
borgmatic/borg/flags.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import itertools
|
||||
|
||||
|
||||
def make_flags(name, value):
|
||||
'''
|
||||
Given a flag name and its value, return it formatted as Borg-compatible flags.
|
||||
'''
|
||||
if not value:
|
||||
return ()
|
||||
|
||||
flag = '--{}'.format(name.replace('_', '-'))
|
||||
|
||||
if value is True:
|
||||
return (flag,)
|
||||
|
||||
return (flag, str(value))
|
||||
|
||||
|
||||
def make_flags_from_arguments(arguments, excludes=()):
|
||||
'''
|
||||
Given borgmatic command-line arguments as an instance of argparse.Namespace, and optionally a
|
||||
list of named arguments to exclude, generate and return the corresponding Borg command-line
|
||||
flags as a tuple.
|
||||
'''
|
||||
return tuple(
|
||||
itertools.chain.from_iterable(
|
||||
make_flags(name, value=getattr(arguments, name))
|
||||
for name in sorted(vars(arguments))
|
||||
if name not in excludes and not name.startswith('_')
|
||||
)
|
||||
)
|
||||
|
|
@ -1,26 +1,43 @@
|
|||
import logging
|
||||
|
||||
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def display_archives_info(
|
||||
repository, storage_config, local_path='borg', remote_path=None, json=False
|
||||
repository, storage_config, info_arguments, local_path='borg', remote_path=None
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, and a storage config dict, display summary information
|
||||
for Borg archives in the repository or return JSON summary information.
|
||||
Given a local or remote repository path, a storage config dict, and the arguments to the info
|
||||
action, display summary information for Borg archives in the repository or return JSON summary
|
||||
information.
|
||||
'''
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
(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 and not json else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
+ (('--json',) if json else ())
|
||||
(local_path, 'info')
|
||||
+ (
|
||||
('--info',)
|
||||
if logger.getEffectiveLevel() == logging.INFO and not info_arguments.json
|
||||
else ()
|
||||
)
|
||||
+ (
|
||||
('--debug', '--show-rc')
|
||||
if logger.isEnabledFor(logging.DEBUG) and not info_arguments.json
|
||||
else ()
|
||||
)
|
||||
+ make_flags('remote-path', remote_path)
|
||||
+ make_flags('lock-wait', lock_wait)
|
||||
+ make_flags_from_arguments(info_arguments, excludes=('repository', 'archive'))
|
||||
+ (
|
||||
'::'.join((repository, info_arguments.archive))
|
||||
if info_arguments.archive
|
||||
else repository,
|
||||
)
|
||||
)
|
||||
|
||||
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
|
||||
return execute_command(
|
||||
full_command, output_log_level=None if info_arguments.json else logging.WARNING
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
import subprocess
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
from borgmatic.execute import execute_command, execute_command_without_capture
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -34,14 +34,15 @@ def initialize_repository(
|
|||
raise
|
||||
|
||||
init_command = (
|
||||
(local_path, 'init', repository)
|
||||
(local_path, 'init')
|
||||
+ (('--encryption', encryption_mode) if encryption_mode else ())
|
||||
+ (('--append-only',) if append_only else ())
|
||||
+ (('--storage-quota', storage_quota) if storage_quota else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
# Don't use execute_command() here because it doesn't support interactive prompts.
|
||||
subprocess.check_call(init_command)
|
||||
execute_command_without_capture(init_command)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,50 @@
|
|||
import logging
|
||||
|
||||
from borgmatic.borg.flags import make_flags, make_flags_from_arguments
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def list_archives(
|
||||
repository, storage_config, archive=None, local_path='borg', remote_path=None, json=False
|
||||
):
|
||||
# A hack to convince Borg to exclude archives ending in ".checkpoint". This assumes that a
|
||||
# non-checkpoint archive name ends in a digit (e.g. from a timestamp).
|
||||
BORG_EXCLUDE_CHECKPOINTS_GLOB = '*[0123456789]'
|
||||
|
||||
|
||||
def list_archives(repository, storage_config, list_arguments, local_path='borg', remote_path=None):
|
||||
'''
|
||||
Given a local or remote repository path and a storage config dict, display the output of listing
|
||||
Borg archives in the repository or return JSON output. Or, if an archive name is given, listing
|
||||
the files in that archive.
|
||||
Given a local or remote repository path, a storage config dict, and the arguments to the list
|
||||
action, display the output of listing Borg archives in the repository or return JSON output. Or,
|
||||
if an archive name is given, listing the files in that archive.
|
||||
'''
|
||||
lock_wait = storage_config.get('lock_wait', None)
|
||||
if list_arguments.successful:
|
||||
list_arguments.glob_archives = BORG_EXCLUDE_CHECKPOINTS_GLOB
|
||||
|
||||
full_command = (
|
||||
(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 and not json else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
+ (('--json',) if json else ())
|
||||
(local_path, 'list')
|
||||
+ (
|
||||
('--info',)
|
||||
if logger.getEffectiveLevel() == logging.INFO and not list_arguments.json
|
||||
else ()
|
||||
)
|
||||
+ (
|
||||
('--debug', '--show-rc')
|
||||
if logger.isEnabledFor(logging.DEBUG) and not list_arguments.json
|
||||
else ()
|
||||
)
|
||||
+ make_flags('remote-path', remote_path)
|
||||
+ make_flags('lock-wait', lock_wait)
|
||||
+ make_flags_from_arguments(
|
||||
list_arguments, excludes=('repository', 'archive', 'successful')
|
||||
)
|
||||
+ (
|
||||
'::'.join((repository, list_arguments.archive))
|
||||
if list_arguments.archive
|
||||
else repository,
|
||||
)
|
||||
)
|
||||
|
||||
return execute_command(full_command, output_log_level=None if json else logging.WARNING)
|
||||
return execute_command(
|
||||
full_command, output_log_level=None if list_arguments.json else logging.WARNING
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,12 +21,15 @@ def _make_prune_flags(retention_config):
|
|||
('--keep-monthly', '6'),
|
||||
)
|
||||
'''
|
||||
if not retention_config.get('prefix'):
|
||||
retention_config['prefix'] = '{hostname}-'
|
||||
config = retention_config.copy()
|
||||
|
||||
if 'prefix' not in config:
|
||||
config['prefix'] = '{hostname}-'
|
||||
elif not config['prefix']:
|
||||
config.pop('prefix')
|
||||
|
||||
return (
|
||||
('--' + option_name.replace('_', '-'), str(retention_config[option_name]))
|
||||
for option_name, value in retention_config.items()
|
||||
('--' + option_name.replace('_', '-'), str(value)) for option_name, value in config.items()
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ def prune_archives(
|
|||
lock_wait = storage_config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
(local_path, 'prune', repository)
|
||||
(local_path, 'prune')
|
||||
+ tuple(element for pair in _make_prune_flags(retention_config) for element in pair)
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
|
|
@ -58,6 +61,7 @@ def prune_archives(
|
|||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--stats',) if stats else ())
|
||||
+ (repository,)
|
||||
)
|
||||
|
||||
execute_command(full_command)
|
||||
execute_command(full_command, output_log_level=logging.WARNING if stats else logging.INFO)
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ SUBPARSER_ALIASES = {
|
|||
}
|
||||
|
||||
|
||||
def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
|
||||
def parse_subparser_arguments(unparsed_arguments, subparsers):
|
||||
'''
|
||||
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
|
||||
object as returned by argparse.ArgumentParser().add_subparsers(), ask each subparser to parse
|
||||
its own arguments and the top-level parser to parse any remaining arguments.
|
||||
Given a sequence of arguments, and a subparsers object as returned by
|
||||
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
|
||||
parsing all arguments. This allows common arguments like "--repository" to be shared across
|
||||
multiple subparsers.
|
||||
|
||||
Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
|
||||
arguments.
|
||||
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
|
||||
'''
|
||||
arguments = collections.OrderedDict()
|
||||
remaining_arguments = list(unparsed_arguments)
|
||||
|
|
@ -31,35 +31,73 @@ def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
|
|||
for alias in aliases
|
||||
}
|
||||
|
||||
# Give each requested action's subparser a shot at parsing all arguments.
|
||||
for subparser_name, subparser in subparsers.choices.items():
|
||||
if subparser_name not in unparsed_arguments:
|
||||
if subparser_name not in remaining_arguments:
|
||||
continue
|
||||
|
||||
remaining_arguments.remove(subparser_name)
|
||||
canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
|
||||
|
||||
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
|
||||
# If a parsed value happens to be the same as the name of a subparser, remove it from the
|
||||
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
|
||||
# the "extract" subparser.
|
||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||
for value in vars(parsed).values():
|
||||
if isinstance(value, str):
|
||||
if value in subparsers.choices:
|
||||
remaining_arguments.remove(value)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
if item in subparsers.choices:
|
||||
remaining_arguments.remove(item)
|
||||
|
||||
arguments[canonical_name] = parsed
|
||||
|
||||
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
||||
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
|
||||
for subparser_name in ('prune', 'create', 'check'):
|
||||
subparser = subparsers.choices[subparser_name]
|
||||
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
|
||||
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
|
||||
arguments[subparser_name] = parsed
|
||||
|
||||
# Then ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
|
||||
# are global arguments.
|
||||
for subparser_name in arguments.keys():
|
||||
subparser = subparsers.choices[subparser_name]
|
||||
parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||
|
||||
arguments['global'] = top_level_parser.parse_args(remaining_arguments)
|
||||
|
||||
return arguments
|
||||
|
||||
|
||||
def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
|
||||
'''
|
||||
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
|
||||
object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
|
||||
arguments as a parsed argparse.Namespace instance.
|
||||
'''
|
||||
# Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
|
||||
# are global arguments.
|
||||
remaining_arguments = list(unparsed_arguments)
|
||||
present_subparser_names = set()
|
||||
|
||||
for subparser_name, subparser in subparsers.choices.items():
|
||||
if subparser_name not in remaining_arguments:
|
||||
continue
|
||||
|
||||
present_subparser_names.add(subparser_name)
|
||||
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||
|
||||
# If no actions are explicitly requested, assume defaults: prune, create, and check.
|
||||
if (
|
||||
not present_subparser_names
|
||||
and '--help' not in unparsed_arguments
|
||||
and '-h' not in unparsed_arguments
|
||||
):
|
||||
for subparser_name in ('prune', 'create', 'check'):
|
||||
subparser = subparsers.choices[subparser_name]
|
||||
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
|
||||
|
||||
# Remove the subparser names themselves.
|
||||
for subparser_name in present_subparser_names:
|
||||
if subparser_name in remaining_arguments:
|
||||
remaining_arguments.remove(subparser_name)
|
||||
|
||||
return top_level_parser.parse_args(remaining_arguments)
|
||||
|
||||
|
||||
def parse_arguments(*unparsed_arguments):
|
||||
'''
|
||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||
|
|
@ -108,7 +146,7 @@ def parse_arguments(*unparsed_arguments):
|
|||
type=int,
|
||||
choices=range(0, 3),
|
||||
default=0,
|
||||
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2)',
|
||||
help='Display verbose progress to syslog (from none to lots: 0, 1, or 2). Ignored when console is interactive',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--version',
|
||||
|
|
@ -212,6 +250,14 @@ def parse_arguments(*unparsed_arguments):
|
|||
add_help=False,
|
||||
)
|
||||
check_group = check_parser.add_argument_group('check arguments')
|
||||
check_group.add_argument(
|
||||
'--only',
|
||||
metavar='CHECK',
|
||||
choices=('repository', 'archives', 'data', 'extract'),
|
||||
dest='only',
|
||||
action='append',
|
||||
help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
|
||||
)
|
||||
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
extract_parser = subparsers.add_parser(
|
||||
|
|
@ -224,7 +270,7 @@ def parse_arguments(*unparsed_arguments):
|
|||
extract_group = extract_parser.add_argument_group('extract arguments')
|
||||
extract_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
||||
help='Path of repository to extract, defaults to the configured repository if there is only one',
|
||||
)
|
||||
extract_group.add_argument('--archive', help='Name of archive to operate on', required=True)
|
||||
extract_group.add_argument(
|
||||
|
|
@ -248,17 +294,54 @@ def parse_arguments(*unparsed_arguments):
|
|||
'list',
|
||||
aliases=SUBPARSER_ALIASES['list'],
|
||||
help='List archives',
|
||||
description='List archives',
|
||||
description='List archives or the contents of an archive',
|
||||
add_help=False,
|
||||
)
|
||||
list_group = list_parser.add_argument_group('list arguments')
|
||||
list_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to use, defaults to the configured repository if there is only one',
|
||||
help='Path of repository to list, defaults to the configured repository if there is only one',
|
||||
)
|
||||
list_group.add_argument('--archive', help='Name of archive to operate on')
|
||||
list_group.add_argument('--archive', help='Name of archive to list')
|
||||
list_group.add_argument(
|
||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||
'--short', default=False, action='store_true', help='Output only archive or path names'
|
||||
)
|
||||
list_group.add_argument('--format', help='Format for file listing')
|
||||
list_group.add_argument(
|
||||
'--json', default=False, action='store_true', help='Output results as JSON'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'-P', '--prefix', help='Only list archive names starting with this prefix'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'-a', '--glob-archives', metavar='GLOB', help='Only list archive names matching this glob'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--successful',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Only list archive names of successful (non-checkpoint) backups',
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--first', metavar='N', help='List first N archives after other filters are applied'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--last', metavar='N', help='List last N archives after other filters are applied'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'-e', '--exclude', metavar='PATTERN', help='Exclude paths matching the pattern'
|
||||
)
|
||||
list_group.add_argument(
|
||||
'--exclude-from', metavar='FILENAME', help='Exclude paths from exclude file, one per line'
|
||||
)
|
||||
list_group.add_argument('--pattern', help='Include or exclude paths matching a pattern')
|
||||
list_group.add_argument(
|
||||
'--patterns-from',
|
||||
metavar='FILENAME',
|
||||
help='Include or exclude paths matching patterns from pattern file, one per line',
|
||||
)
|
||||
list_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
|
|
@ -270,12 +353,38 @@ def parse_arguments(*unparsed_arguments):
|
|||
add_help=False,
|
||||
)
|
||||
info_group = info_parser.add_argument_group('info arguments')
|
||||
info_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to show info for, defaults to the configured repository if there is only one',
|
||||
)
|
||||
info_group.add_argument('--archive', help='Name of archive to show info for')
|
||||
info_group.add_argument(
|
||||
'--json', dest='json', default=False, action='store_true', help='Output results as JSON'
|
||||
)
|
||||
info_group.add_argument(
|
||||
'-P', '--prefix', help='Only show info for archive names starting with this prefix'
|
||||
)
|
||||
info_group.add_argument(
|
||||
'-a',
|
||||
'--glob-archives',
|
||||
metavar='GLOB',
|
||||
help='Only show info for archive names matching this glob',
|
||||
)
|
||||
info_group.add_argument(
|
||||
'--sort-by', metavar='KEYS', help='Comma-separated list of sorting keys'
|
||||
)
|
||||
info_group.add_argument(
|
||||
'--first',
|
||||
metavar='N',
|
||||
help='Show info for first N archives after other filters are applied',
|
||||
)
|
||||
info_group.add_argument(
|
||||
'--last', metavar='N', help='Show info for first N archives after other filters are applied'
|
||||
)
|
||||
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers)
|
||||
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
|
||||
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
|
||||
|
||||
if arguments['global'].excludes_filename:
|
||||
raise ValueError(
|
||||
|
|
@ -285,6 +394,9 @@ def parse_arguments(*unparsed_arguments):
|
|||
if 'init' in arguments and arguments['global'].dry_run:
|
||||
raise ValueError('The init action cannot be used with the --dry-run option')
|
||||
|
||||
if 'list' in arguments and arguments['list'].glob_archives and arguments['list'].successful:
|
||||
raise ValueError('The --glob-archives and --successful options cannot be used together')
|
||||
|
||||
if (
|
||||
'list' in arguments
|
||||
and 'info' in arguments
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from subprocess import CalledProcessError
|
|||
import colorama
|
||||
import pkg_resources
|
||||
|
||||
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
|
||||
|
|
@ -19,6 +18,7 @@ from borgmatic.borg import list as borg_list
|
|||
from borgmatic.borg import prune as borg_prune
|
||||
from borgmatic.commands.arguments import parse_arguments
|
||||
from borgmatic.config import checks, collect, convert, validate
|
||||
from borgmatic.hooks import command, healthchecks, postgresql
|
||||
from borgmatic.logger import configure_logging, should_do_markup
|
||||
from borgmatic.signals import configure_signals
|
||||
from borgmatic.verbosity import verbosity_to_log_level
|
||||
|
|
@ -28,13 +28,16 @@ logger = logging.getLogger(__name__)
|
|||
LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||
|
||||
|
||||
def run_configuration(config_filename, config, arguments): # pragma: no cover
|
||||
def run_configuration(config_filename, config, arguments):
|
||||
'''
|
||||
Given a config filename, the corresponding parsed config dict, and command-line arguments as a
|
||||
dict from subparser name to a namespace of parsed arguments, execute its defined pruning,
|
||||
backups, consistency checks, and/or other actions.
|
||||
|
||||
Yield JSON output strings from executing any actions that produce JSON.
|
||||
Yield a combination of:
|
||||
|
||||
* JSON output strings from successfully executing any actions that produce JSON
|
||||
* logging.LogRecord instances containing errors from any actions or backup hooks that fail
|
||||
'''
|
||||
(location, storage, retention, consistency, hooks) = (
|
||||
config.get(section_name, {})
|
||||
|
|
@ -42,49 +45,93 @@ def run_configuration(config_filename, config, arguments): # pragma: no cover
|
|||
)
|
||||
global_arguments = arguments['global']
|
||||
|
||||
try:
|
||||
local_path = location.get('local_path', 'borg')
|
||||
remote_path = location.get('remote_path')
|
||||
borg_environment.initialize(storage)
|
||||
local_path = location.get('local_path', 'borg')
|
||||
remote_path = location.get('remote_path')
|
||||
borg_environment.initialize(storage)
|
||||
encountered_error = None
|
||||
error_repository = ''
|
||||
|
||||
if 'create' in arguments:
|
||||
hook.execute_hook(
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
command.execute_hook(
|
||||
hooks.get('before_backup'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'pre-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
for repository_path in location['repositories']:
|
||||
yield from run_actions(
|
||||
arguments=arguments,
|
||||
location=location,
|
||||
storage=storage,
|
||||
retention=retention,
|
||||
consistency=consistency,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
repository_path=repository_path,
|
||||
postgresql.dump_databases(
|
||||
hooks.get('postgresql_databases'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'start'
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running pre-backup hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
if 'create' in arguments:
|
||||
hook.execute_hook(
|
||||
if not encountered_error:
|
||||
for repository_path in location['repositories']:
|
||||
try:
|
||||
yield from run_actions(
|
||||
arguments=arguments,
|
||||
location=location,
|
||||
storage=storage,
|
||||
retention=retention,
|
||||
consistency=consistency,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
repository_path=repository_path,
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = error
|
||||
error_repository = repository_path
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running actions for repository'.format(repository_path), error
|
||||
)
|
||||
|
||||
if 'create' in arguments and not encountered_error:
|
||||
try:
|
||||
command.execute_hook(
|
||||
hooks.get('after_backup'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'post-backup',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
except (OSError, CalledProcessError):
|
||||
hook.execute_hook(
|
||||
hooks.get('on_error'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'on-error',
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
raise
|
||||
postgresql.remove_database_dumps(
|
||||
hooks.get('postgresql_databases'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
encountered_error = error
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running post-backup hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
if encountered_error:
|
||||
try:
|
||||
command.execute_hook(
|
||||
hooks.get('on_error'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'on-error',
|
||||
global_arguments.dry_run,
|
||||
repository=error_repository,
|
||||
error=encountered_error,
|
||||
output=getattr(encountered_error, 'output', ''),
|
||||
)
|
||||
healthchecks.ping_healthchecks(
|
||||
hooks.get('healthchecks'), config_filename, global_arguments.dry_run, 'fail'
|
||||
)
|
||||
except (OSError, CalledProcessError) as error:
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running on-error hook'.format(config_filename), error
|
||||
)
|
||||
|
||||
|
||||
def run_actions(
|
||||
|
|
@ -147,7 +194,12 @@ def run_actions(
|
|||
if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency):
|
||||
logger.info('{}: Running consistency checks'.format(repository))
|
||||
borg_check.check_archives(
|
||||
repository, storage, consistency, local_path=local_path, remote_path=remote_path
|
||||
repository,
|
||||
storage,
|
||||
consistency,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
only_checks=arguments['check'].only,
|
||||
)
|
||||
if 'extract' in arguments:
|
||||
if arguments['extract'].repository is None or repository == arguments['extract'].repository:
|
||||
|
|
@ -171,24 +223,24 @@ def run_actions(
|
|||
json_output = borg_list.list_archives(
|
||||
repository,
|
||||
storage,
|
||||
arguments['list'].archive,
|
||||
list_arguments=arguments['list'],
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
json=arguments['list'].json,
|
||||
)
|
||||
if json_output:
|
||||
yield json.loads(json_output)
|
||||
if 'info' in arguments:
|
||||
logger.info('{}: Displaying summary info for archives'.format(repository))
|
||||
json_output = borg_info.display_archives_info(
|
||||
repository,
|
||||
storage,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
json=arguments['info'].json,
|
||||
)
|
||||
if json_output:
|
||||
yield json.loads(json_output)
|
||||
if arguments['info'].repository is None or repository == arguments['info'].repository:
|
||||
logger.info('{}: Displaying summary info for archives'.format(repository))
|
||||
json_output = borg_info.display_archives_info(
|
||||
repository,
|
||||
storage,
|
||||
info_arguments=arguments['info'],
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
if json_output:
|
||||
yield json.loads(json_output)
|
||||
|
||||
|
||||
def load_configurations(config_filenames):
|
||||
|
|
@ -226,6 +278,38 @@ def load_configurations(config_filenames):
|
|||
return (configs, logs)
|
||||
|
||||
|
||||
def make_error_log_records(message, error=None):
|
||||
'''
|
||||
Given error message text and an optional exception object, yield a series of logging.LogRecord
|
||||
instances with error summary information.
|
||||
'''
|
||||
if not error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
raise error
|
||||
except CalledProcessError as error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
)
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error.output)
|
||||
)
|
||||
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error))
|
||||
except (ValueError, OSError) as error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=message)
|
||||
)
|
||||
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error))
|
||||
except: # noqa: E722
|
||||
# Raising above only as a means of determining the error type. Swallow the exception here
|
||||
# because we don't want the exception to propagate out of this function.
|
||||
pass
|
||||
|
||||
|
||||
def collect_configuration_run_summary_logs(configs, arguments):
|
||||
'''
|
||||
Given a dict of configuration filename to corresponding parsed configuration, and parsed
|
||||
|
|
@ -248,16 +332,42 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
|||
try:
|
||||
validate.guard_configuration_contains_repository(repository, configs)
|
||||
except ValueError as error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
||||
)
|
||||
yield from make_error_log_records(str(error))
|
||||
return
|
||||
|
||||
if not configs:
|
||||
yield from make_error_log_records(
|
||||
'{}: No configuration files found'.format(' '.join(arguments['global'].config_paths))
|
||||
)
|
||||
return
|
||||
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
hooks = config.get('hooks', {})
|
||||
command.execute_hook(
|
||||
hooks.get('before_everything'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'pre-everything',
|
||||
arguments['global'].dry_run,
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from make_error_log_records('Error running pre-everything hook', error)
|
||||
return
|
||||
|
||||
# Execute the actions corresponding to each configuration file.
|
||||
json_results = []
|
||||
for config_filename, config in configs.items():
|
||||
try:
|
||||
json_results.extend(list(run_configuration(config_filename, config, arguments)))
|
||||
results = list(run_configuration(config_filename, config, arguments))
|
||||
error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
|
||||
|
||||
if error_logs:
|
||||
yield from make_error_log_records(
|
||||
'{}: Error running configuration file'.format(config_filename)
|
||||
)
|
||||
yield from error_logs
|
||||
else:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.INFO,
|
||||
|
|
@ -265,31 +375,25 @@ def collect_configuration_run_summary_logs(configs, arguments):
|
|||
msg='{}: Successfully ran configuration file'.format(config_filename),
|
||||
)
|
||||
)
|
||||
except (ValueError, OSError, CalledProcessError) as error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
levelname='CRITICAL',
|
||||
msg='{}: Error running configuration file'.format(config_filename),
|
||||
)
|
||||
)
|
||||
yield logging.makeLogRecord(
|
||||
dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg=error)
|
||||
)
|
||||
if results:
|
||||
json_results.extend(results)
|
||||
|
||||
if json_results:
|
||||
sys.stdout.write(json.dumps(json_results))
|
||||
|
||||
if not configs:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
levelname='CRITICAL',
|
||||
msg='{}: No configuration files found'.format(
|
||||
' '.join(arguments['global'].config_paths)
|
||||
),
|
||||
)
|
||||
)
|
||||
if 'create' in arguments:
|
||||
try:
|
||||
for config_filename, config in configs.items():
|
||||
hooks = config.get('hooks', {})
|
||||
command.execute_hook(
|
||||
hooks.get('after_everything'),
|
||||
hooks.get('umask'),
|
||||
config_filename,
|
||||
'post-everything',
|
||||
arguments['global'].dry_run,
|
||||
)
|
||||
except (CalledProcessError, ValueError, OSError) as error:
|
||||
yield from make_error_log_records('Error running post-everything hook', error)
|
||||
|
||||
|
||||
def exit_with_help_link(): # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ def convert_legacy_parsed_config(source_config, source_excludes, schema):
|
|||
destination_config['consistency']['checks'] = source_config.consistency['checks'].split(' ')
|
||||
|
||||
# Add comments to each section, and then add comments to the fields in each section.
|
||||
generate.add_comments_to_configuration(destination_config, schema)
|
||||
generate.add_comments_to_configuration_map(destination_config, schema)
|
||||
|
||||
for section_name, section_config in destination_config.items():
|
||||
generate.add_comments_to_configuration(
|
||||
generate.add_comments_to_configuration_map(
|
||||
section_config, schema['map'][section_name], indent=generate.INDENT
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from ruamel import yaml
|
||||
|
||||
INDENT = 4
|
||||
SEQUENCE_INDENT = 2
|
||||
|
||||
|
||||
def _insert_newline_before_comment(config, field_name):
|
||||
|
|
@ -15,7 +18,7 @@ def _insert_newline_before_comment(config, field_name):
|
|||
)
|
||||
|
||||
|
||||
def _schema_to_sample_configuration(schema, level=0):
|
||||
def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
'''
|
||||
Given a loaded configuration schema, generate and return sample config for it. Include comments
|
||||
for each section based on the schema "desc" description.
|
||||
|
|
@ -24,14 +27,29 @@ def _schema_to_sample_configuration(schema, level=0):
|
|||
if example is not None:
|
||||
return example
|
||||
|
||||
config = yaml.comments.CommentedMap(
|
||||
[
|
||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
||||
for section_name, section_schema in schema['map'].items()
|
||||
]
|
||||
)
|
||||
|
||||
add_comments_to_configuration(config, schema, indent=(level * INDENT))
|
||||
if 'seq' in schema:
|
||||
config = yaml.comments.CommentedSeq(
|
||||
[
|
||||
_schema_to_sample_configuration(item_schema, level, parent_is_sequence=True)
|
||||
for item_schema in schema['seq']
|
||||
]
|
||||
)
|
||||
add_comments_to_configuration_sequence(
|
||||
config, schema, indent=(level * INDENT) + SEQUENCE_INDENT
|
||||
)
|
||||
elif 'map' in schema:
|
||||
config = yaml.comments.CommentedMap(
|
||||
[
|
||||
(section_name, _schema_to_sample_configuration(section_schema, level + 1))
|
||||
for section_name, section_schema in schema['map'].items()
|
||||
]
|
||||
)
|
||||
indent = (level * INDENT) + (SEQUENCE_INDENT if parent_is_sequence else 0)
|
||||
add_comments_to_configuration_map(
|
||||
config, schema, indent=indent, skip_first=parent_is_sequence
|
||||
)
|
||||
else:
|
||||
raise ValueError('Schema at level {} is unsupported: {}'.format(level, schema))
|
||||
|
||||
return config
|
||||
|
||||
|
|
@ -42,13 +60,12 @@ def _comment_out_line(line):
|
|||
if not stripped_line or stripped_line.startswith('#'):
|
||||
return line
|
||||
|
||||
# Comment out the names of optional sections.
|
||||
one_indent = ' ' * INDENT
|
||||
if not line.startswith(one_indent):
|
||||
return '#' + line
|
||||
# Comment out the names of optional sections, inserting the '#' after any indent for aesthetics.
|
||||
matches = re.match(r'(\s*)', line)
|
||||
indent_spaces = matches.group(0) if matches else ''
|
||||
count_indent_spaces = len(indent_spaces)
|
||||
|
||||
# Otherwise, comment out the line, but insert the "#" after the first indent for aesthetics.
|
||||
return '#'.join((one_indent, line[INDENT:]))
|
||||
return '# '.join((indent_spaces, line[count_indent_spaces:]))
|
||||
|
||||
|
||||
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
|
||||
|
|
@ -90,7 +107,12 @@ def _render_configuration(config):
|
|||
'''
|
||||
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
||||
'''
|
||||
return yaml.round_trip_dump(config, indent=INDENT, block_seq_indent=INDENT)
|
||||
dumper = yaml.YAML()
|
||||
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
||||
rendered = io.StringIO()
|
||||
dumper.dump(config, rendered)
|
||||
|
||||
return rendered.getvalue()
|
||||
|
||||
|
||||
def write_configuration(config_filename, rendered_config, mode=0o600):
|
||||
|
|
@ -112,13 +134,49 @@ def write_configuration(config_filename, rendered_config, mode=0o600):
|
|||
os.chmod(config_filename, mode)
|
||||
|
||||
|
||||
def add_comments_to_configuration(config, schema, indent=0):
|
||||
def add_comments_to_configuration_sequence(config, schema, indent=0):
|
||||
'''
|
||||
If the given config sequence's items are maps, then mine the schema for the description of the
|
||||
map's first item, and slap that atop the sequence. Indent the comment the given number of
|
||||
characters.
|
||||
|
||||
Doing this for sequences of maps results in nice comments that look like:
|
||||
|
||||
```
|
||||
things:
|
||||
# First key description. Added by this function.
|
||||
- key: foo
|
||||
# Second key description. Added by add_comments_to_configuration_map().
|
||||
other: bar
|
||||
```
|
||||
'''
|
||||
if 'map' not in schema['seq'][0]:
|
||||
return
|
||||
|
||||
for field_name in config[0].keys():
|
||||
field_schema = schema['seq'][0]['map'].get(field_name, {})
|
||||
description = field_schema.get('desc')
|
||||
|
||||
# No description to use? Skip it.
|
||||
if not field_schema or not description:
|
||||
return
|
||||
|
||||
config[0].yaml_set_start_comment(description, indent=indent)
|
||||
|
||||
# We only want the first key's description here, as the rest of the keys get commented by
|
||||
# add_comments_to_configuration_map().
|
||||
return
|
||||
|
||||
|
||||
def add_comments_to_configuration_map(config, schema, indent=0, skip_first=False):
|
||||
'''
|
||||
Using descriptions from a schema as a source, add those descriptions as comments to the given
|
||||
config before each field. This function only adds comments for the top-most config map level.
|
||||
Indent the comment the given number of characters.
|
||||
config mapping, before each field. Indent the comment the given number of characters.
|
||||
'''
|
||||
for index, field_name in enumerate(config.keys()):
|
||||
if skip_first and index == 0:
|
||||
continue
|
||||
|
||||
field_schema = schema['map'].get(field_name, {})
|
||||
description = field_schema.get('desc')
|
||||
|
||||
|
|
@ -127,6 +185,7 @@ def add_comments_to_configuration(config, schema, indent=0):
|
|||
continue
|
||||
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before=description, indent=indent)
|
||||
|
||||
if index > 0:
|
||||
_insert_newline_before_comment(config, field_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ map:
|
|||
source_directories:
|
||||
required: true
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
List of source directories to backup (required). Globs and tildes are expanded.
|
||||
example:
|
||||
|
|
@ -21,7 +21,7 @@ map:
|
|||
repositories:
|
||||
required: true
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Paths to local or remote repositories (required). Tildes are expanded. Multiple
|
||||
repositories are backed up to in sequence. See ssh_command for SSH options like
|
||||
|
|
@ -36,6 +36,18 @@ map:
|
|||
type: bool
|
||||
desc: Only store/extract numeric user and group identifiers. Defaults to false.
|
||||
example: true
|
||||
atime:
|
||||
type: bool
|
||||
desc: Store atime into archive. Defaults to true.
|
||||
example: false
|
||||
ctime:
|
||||
type: bool
|
||||
desc: Store ctime into archive. Defaults to true.
|
||||
example: false
|
||||
birthtime:
|
||||
type: bool
|
||||
desc: Store birthtime (creation date) into archive. Defaults to true.
|
||||
example: false
|
||||
read_special:
|
||||
type: bool
|
||||
desc: |
|
||||
|
|
@ -48,23 +60,23 @@ map:
|
|||
desc: Record bsdflags (e.g. NODUMP, IMMUTABLE) in archive. Defaults to true.
|
||||
example: true
|
||||
files_cache:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Mode in which to operate the files cache. See
|
||||
https://borgbackup.readthedocs.io/en/stable/usage/create.html#description for
|
||||
details. Defaults to "ctime,size,inode".
|
||||
example: ctime,size,inode
|
||||
local_path:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: Alternate Borg local executable. Defaults to "borg".
|
||||
example: borg1
|
||||
remote_path:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: Alternate Borg remote executable. Defaults to "borg".
|
||||
example: borg1
|
||||
patterns:
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Any paths matching these patterns are included/excluded from backups. Globs are
|
||||
expanded. (Tildes are not.) Note that Borg considers this option experimental.
|
||||
|
|
@ -77,7 +89,7 @@ map:
|
|||
- '- /home/*'
|
||||
patterns_from:
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Read include/exclude patterns from one or more separate named files, one pattern
|
||||
per line. Note that Borg considers this option experimental. See the output of
|
||||
|
|
@ -86,7 +98,7 @@ map:
|
|||
- /etc/borgmatic/patterns
|
||||
exclude_patterns:
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Any paths matching these patterns are excluded from backups. Globs and tildes
|
||||
are expanded. See the output of "borg help patterns" for more details.
|
||||
|
|
@ -96,7 +108,7 @@ map:
|
|||
- /etc/ssl
|
||||
exclude_from:
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Read exclude patterns from one or more separate named files, one pattern per
|
||||
line. See the output of "borg help patterns" for more details.
|
||||
|
|
@ -109,7 +121,7 @@ map:
|
|||
http://www.brynosaurus.com/cachedir/spec.html for details. Defaults to false.
|
||||
example: true
|
||||
exclude_if_present:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Exclude directories that contain a file with the given filename. Defaults to not
|
||||
set.
|
||||
|
|
@ -122,7 +134,7 @@ map:
|
|||
details.
|
||||
map:
|
||||
encryption_passcommand:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
The standard output of this command is used to unlock the encryption key. Only
|
||||
use on repositories that were initialized with passcommand/repokey encryption.
|
||||
|
|
@ -130,7 +142,7 @@ map:
|
|||
then encryption_passphrase takes precedence. Defaults to not set.
|
||||
example: "secret-tool lookup borg-repository repo-name"
|
||||
encryption_passphrase:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Passphrase to unlock the encryption key with. Only use on repositories that were
|
||||
initialized with passphrase/repokey encryption. Quote the value if it contains
|
||||
|
|
@ -145,14 +157,14 @@ map:
|
|||
for details. Defaults to checkpoints every 1800 seconds (30 minutes).
|
||||
example: 1800
|
||||
chunker_params:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Specify the parameters passed to then chunker (CHUNK_MIN_EXP, CHUNK_MAX_EXP,
|
||||
HASH_MASK_BITS, HASH_WINDOW_SIZE). See https://borgbackup.readthedocs.io/en/stable/internals.html
|
||||
for details. Defaults to "19,23,21,4095".
|
||||
example: 19,23,21,4095
|
||||
compression:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Type of compression to use when creating archives. See
|
||||
https://borgbackup.readthedocs.org/en/stable/usage.html#borg-create for details.
|
||||
|
|
@ -163,34 +175,34 @@ map:
|
|||
desc: Remote network upload rate limit in kiBytes/second. Defaults to unlimited.
|
||||
example: 100
|
||||
ssh_command:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
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
|
||||
type: str
|
||||
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
|
||||
type: str
|
||||
desc: |
|
||||
Path for Borg configuration files. Defaults to $borg_base_directory/.config/borg
|
||||
example: /path/to/base/config
|
||||
borg_cache_directory:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Path for Borg cache files. Defaults to $borg_base_directory/.cache/borg
|
||||
example: /path/to/base/cache
|
||||
borg_security_directory:
|
||||
type: scalar
|
||||
type: str
|
||||
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
|
||||
type: str
|
||||
desc: |
|
||||
Path for Borg encryption key files. Defaults to $borg_base_directory/.config/borg/keys
|
||||
example: /path/to/base/config/keys
|
||||
|
|
@ -203,7 +215,7 @@ map:
|
|||
desc: Maximum seconds to wait for acquiring a repository/cache lock. Defaults to 1.
|
||||
example: 5
|
||||
archive_name_format:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
Name of the archive. Borg placeholders can be used. See the output of
|
||||
"borg help placeholders" for details. Defaults to
|
||||
|
|
@ -212,6 +224,16 @@ map:
|
|||
archives with a different archive name format. And you should also specify a
|
||||
prefix in the consistency section as well.
|
||||
example: "{hostname}-documents-{now}"
|
||||
relocated_repo_access_is_ok:
|
||||
type: bool
|
||||
desc: Bypass Borg error about a repository that has been moved. Defaults to false.
|
||||
example: true
|
||||
unknown_unencrypted_repo_access_is_ok:
|
||||
type: bool
|
||||
desc: |
|
||||
Bypass Borg error about a previously unknown unencrypted repository. Defaults to
|
||||
false.
|
||||
example: true
|
||||
retention:
|
||||
desc: |
|
||||
Retention policy for how many backups to keep in each category. See
|
||||
|
|
@ -221,7 +243,7 @@ map:
|
|||
if you'd like to skip pruning entirely.
|
||||
map:
|
||||
keep_within:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: Keep all archives within this time interval.
|
||||
example: 3H
|
||||
keep_secondly:
|
||||
|
|
@ -253,11 +275,11 @@ map:
|
|||
desc: Number of yearly archives to keep.
|
||||
example: 1
|
||||
prefix:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
When pruning, only consider archive names starting with this prefix.
|
||||
Borg placeholders can be used. See the output of "borg help placeholders" for
|
||||
details. Defaults to "{hostname}-".
|
||||
details. Defaults to "{hostname}-". Use an empty value to disable the default.
|
||||
example: sourcehostname
|
||||
consistency:
|
||||
desc: |
|
||||
|
|
@ -268,20 +290,21 @@ map:
|
|||
checks:
|
||||
seq:
|
||||
- type: str
|
||||
enum: ['repository', 'archives', 'extract', 'disabled']
|
||||
enum: ['repository', 'archives', 'data', 'extract', 'disabled']
|
||||
unique: true
|
||||
desc: |
|
||||
List of one or more consistency checks to run: "repository", "archives", and/or
|
||||
"extract". Defaults to "repository" and "archives". Set to "disabled" to disable
|
||||
all consistency checks. "repository" checks the consistency of the repository,
|
||||
"archive" checks all of the archives, and "extract" does an extraction dry-run
|
||||
of the most recent archive.
|
||||
List of one or more consistency checks to run: "repository", "archives", "data",
|
||||
and/or "extract". Defaults to "repository" and "archives". Set to "disabled" to
|
||||
disable all consistency checks. "repository" checks the consistency of the
|
||||
repository, "archives" checks all of the archives, "data" verifies the integrity
|
||||
of the data within the archives, and "extract" does an extraction dry-run of the
|
||||
most recent archive. Note that "data" implies "archives".
|
||||
example:
|
||||
- repository
|
||||
- archives
|
||||
check_repositories:
|
||||
seq:
|
||||
- type: scalar
|
||||
- type: str
|
||||
desc: |
|
||||
Paths to a subset of the repositories in the location section on which to run
|
||||
consistency checks. Handy in case some of your repositories are very large, and
|
||||
|
|
@ -295,11 +318,12 @@ map:
|
|||
"archives" check. Defaults to checking all archives.
|
||||
example: 3
|
||||
prefix:
|
||||
type: scalar
|
||||
type: str
|
||||
desc: |
|
||||
When performing the "archives" check, only consider archive names starting with
|
||||
this prefix. Borg placeholders can be used. See the output of
|
||||
"borg help placeholders" for details. Defaults to "{hostname}-".
|
||||
"borg help placeholders" for details. Defaults to "{hostname}-". Use an empty
|
||||
value to disable the default.
|
||||
example: sourcehostname
|
||||
output:
|
||||
desc: |
|
||||
|
|
@ -313,29 +337,115 @@ map:
|
|||
example: false
|
||||
hooks:
|
||||
desc: |
|
||||
Shell commands or scripts to execute before and after a backup or if an error has occurred.
|
||||
IMPORTANT: All provided commands and scripts are executed with user permissions of borgmatic.
|
||||
Do not forget to set secure permissions on this file as well as on any script listed (chmod 0700) to
|
||||
prevent potential shell injection or privilege escalation.
|
||||
Shell commands, scripts, or integrations to execute at various points during a borgmatic
|
||||
run. IMPORTANT: All provided commands and scripts are executed with user permissions of
|
||||
borgmatic. Do not forget to set secure permissions on this configuration file (chmod
|
||||
0600) as well as on any script called from a hook (chmod 0700) to prevent potential
|
||||
shell injection or privilege escalation.
|
||||
map:
|
||||
before_backup:
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: List of one or more shell commands or scripts to execute before creating a backup.
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute before creating a
|
||||
backup, run once per configuration file.
|
||||
example:
|
||||
- echo "Starting a backup job."
|
||||
- echo "Starting a backup."
|
||||
after_backup:
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: List of one or more shell commands or scripts to execute after creating a backup.
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute after creating a
|
||||
backup, run once per configuration file.
|
||||
example:
|
||||
- echo "Backup created."
|
||||
- echo "Created a backup."
|
||||
on_error:
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: List of one or more shell commands or scripts to execute in case an exception has occurred.
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute when an exception
|
||||
occurs during a backup or when running a before_backup or after_backup hook.
|
||||
example:
|
||||
- echo "Error while creating a backup."
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
postgresql_databases:
|
||||
seq:
|
||||
- map:
|
||||
name:
|
||||
required: true
|
||||
type: str
|
||||
desc: |
|
||||
Database name (required if using this hook). Or "all" to dump all
|
||||
databases on the host.
|
||||
example: users
|
||||
hostname:
|
||||
type: str
|
||||
desc: |
|
||||
Database hostname to connect to. Defaults to connecting via local
|
||||
Unix socket.
|
||||
example: database.example.org
|
||||
port:
|
||||
type: int
|
||||
desc: Port to connect to. Defaults to 5432.
|
||||
example: 5433
|
||||
username:
|
||||
type: str
|
||||
desc: |
|
||||
Username with which to connect to the database. Defaults to the
|
||||
username of the current user. You probably want to specify the
|
||||
"postgres" superuser here when the database name is "all".
|
||||
example: dbuser
|
||||
password:
|
||||
type: str
|
||||
desc: |
|
||||
Password with which to connect to the database. Omitting a password
|
||||
will only work if PostgreSQL is configured to trust the configured
|
||||
username without a password, or you create a ~/.pgpass file.
|
||||
example: trustsome1
|
||||
format:
|
||||
type: str
|
||||
enum: ['plain', 'custom', 'directory', 'tar']
|
||||
desc: |
|
||||
Database dump output format. One of "plain", "custom", "directory",
|
||||
or "tar". Defaults to "custom" (unlike raw pg_dump). See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
Note that format is ignored when the database name is "all".
|
||||
example: directory
|
||||
options:
|
||||
type: str
|
||||
desc: |
|
||||
Additional pg_dump/pg_dumpall options to pass directly to the dump
|
||||
command, without performing any validation on them. See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
example: --role=someone
|
||||
desc: |
|
||||
List of one or more PostgreSQL databases to dump before creating a backup,
|
||||
run once per configuration file. The database dumps are added to your source
|
||||
directories at runtime, backed up, and then removed afterwards. Requires
|
||||
pg_dump/pg_dumpall/pg_restore commands. See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html for details.
|
||||
healthchecks:
|
||||
type: str
|
||||
desc: |
|
||||
Healthchecks ping URL or UUID to notify when a backup begins, ends, or errors.
|
||||
Create an account at https://healthchecks.io if you'd like to use this service.
|
||||
example:
|
||||
https://hc-ping.com/your-uuid-here
|
||||
before_everything:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute before running all
|
||||
actions (if one of them is "create"), run once before all configuration files.
|
||||
example:
|
||||
- echo "Starting actions."
|
||||
after_everything:
|
||||
seq:
|
||||
- type: str
|
||||
desc: |
|
||||
List of one or more shell commands or scripts to execute after running all
|
||||
actions (if one of them is "create"), run once after all configuration files.
|
||||
example:
|
||||
- echo "Completed actions."
|
||||
umask:
|
||||
type: scalar
|
||||
desc: Umask used when executing hooks. Defaults to the umask that borgmatic is run with.
|
||||
|
|
|
|||
|
|
@ -64,6 +64,23 @@ def apply_logical_validation(config_filename, parsed_configuration):
|
|||
)
|
||||
|
||||
|
||||
def remove_examples(schema):
|
||||
'''
|
||||
pykwalify gets angry if the example field is not a string. So rather than bend to its will,
|
||||
remove all examples from the given schema before passing the schema to pykwalify.
|
||||
'''
|
||||
if 'map' in schema:
|
||||
for item_name, item_schema in schema['map'].items():
|
||||
item_schema.pop('example', None)
|
||||
remove_examples(item_schema)
|
||||
elif 'seq' in schema:
|
||||
for item_schema in schema['seq']:
|
||||
item_schema.pop('example', None)
|
||||
remove_examples(item_schema)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def parse_configuration(config_filename, schema_filename):
|
||||
'''
|
||||
Given the path to a config filename in YAML format and the path to a schema filename in
|
||||
|
|
@ -84,13 +101,7 @@ def parse_configuration(config_filename, schema_filename):
|
|||
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
|
||||
raise Validation_error(config_filename, (str(error),))
|
||||
|
||||
# pykwalify gets angry if the example field is not a string. So rather than bend to its will,
|
||||
# remove all examples before passing the schema to pykwalify.
|
||||
for section_name, section_schema in schema['map'].items():
|
||||
for field_name, field_schema in section_schema['map'].items():
|
||||
field_schema.pop('example', None)
|
||||
|
||||
validator = pykwalify.core.Core(source_data=config, schema_data=schema)
|
||||
validator = pykwalify.core.Core(source_data=config, schema_data=remove_examples(schema))
|
||||
parsed_result = validator.validate(raise_exception=False)
|
||||
|
||||
if validator.validation_errors:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,25 @@
|
|||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def execute_and_log_output(full_command, output_log_level, shell):
|
||||
ERROR_OUTPUT_MAX_LINE_COUNT = 25
|
||||
BORG_ERROR_EXIT_CODE = 2
|
||||
|
||||
|
||||
def borg_command(full_command):
|
||||
'''
|
||||
Return True if this is a Borg command, or False if it's some other command.
|
||||
'''
|
||||
return 'borg' in full_command[0]
|
||||
|
||||
|
||||
def execute_and_log_output(full_command, output_log_level, shell, environment):
|
||||
last_lines = []
|
||||
process = subprocess.Popen(
|
||||
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell
|
||||
full_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell, env=environment
|
||||
)
|
||||
|
||||
while process.poll() is None:
|
||||
|
|
@ -14,30 +27,68 @@ def execute_and_log_output(full_command, output_log_level, shell):
|
|||
if not line:
|
||||
continue
|
||||
|
||||
if line.startswith('borg: error:'):
|
||||
logger.error(line)
|
||||
else:
|
||||
logger.log(output_log_level, line)
|
||||
# Keep the last few lines of output in case the command errors, and we need the output for
|
||||
# the exception below.
|
||||
last_lines.append(line)
|
||||
if len(last_lines) > ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
last_lines.pop(0)
|
||||
|
||||
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)
|
||||
|
||||
# If we're running something other than Borg, treat all non-zero exit codes as errors.
|
||||
if borg_command(full_command):
|
||||
error = bool(exit_code >= BORG_ERROR_EXIT_CODE)
|
||||
else:
|
||||
error = bool(exit_code != 0)
|
||||
|
||||
if error:
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
# inadvertently hide error output.
|
||||
if len(last_lines) == ERROR_OUTPUT_MAX_LINE_COUNT:
|
||||
last_lines.insert(0, '...')
|
||||
|
||||
raise subprocess.CalledProcessError(
|
||||
exit_code, ' '.join(full_command), '\n'.join(last_lines)
|
||||
)
|
||||
|
||||
|
||||
def execute_command(full_command, output_log_level=logging.INFO, shell=False):
|
||||
def execute_command(
|
||||
full_command, output_log_level=logging.INFO, shell=False, extra_environment=None
|
||||
):
|
||||
'''
|
||||
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.
|
||||
shell is True, execute the command within a shell. If an extra environment dict is given, then
|
||||
use it to augment the current environment, and pass the result into the command.
|
||||
|
||||
Raise subprocesses.CalledProcessError if an error occurs while running the command.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
environment = {**os.environ, **extra_environment} if extra_environment else None
|
||||
|
||||
if output_log_level is None:
|
||||
output = subprocess.check_output(full_command, shell=shell, env=environment)
|
||||
return output.decode() if output is not None else None
|
||||
else:
|
||||
execute_and_log_output(full_command, output_log_level, shell=shell, environment=environment)
|
||||
|
||||
|
||||
def execute_command_without_capture(full_command):
|
||||
'''
|
||||
Execute the given command (a sequence of command/argument strings), but don't capture or log its
|
||||
output in any way. This is necessary for commands that monkey with the terminal (e.g. progress
|
||||
display) or provide interactive prompts.
|
||||
'''
|
||||
logger.debug(' '.join(full_command))
|
||||
|
||||
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)
|
||||
try:
|
||||
subprocess.check_call(full_command)
|
||||
except subprocess.CalledProcessError as error:
|
||||
if error.returncode >= BORG_ERROR_EXIT_CODE:
|
||||
raise
|
||||
|
|
|
|||
0
borgmatic/hooks/__init__.py
Normal file
0
borgmatic/hooks/__init__.py
Normal file
|
|
@ -6,13 +6,28 @@ from borgmatic import execute
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def execute_hook(commands, umask, config_filename, description, dry_run):
|
||||
def interpolate_context(command, context):
|
||||
'''
|
||||
Given a single hook command and a dict of context names/values, interpolate the values by
|
||||
"{name}" into the command and return the result.
|
||||
'''
|
||||
for name, value in context.items():
|
||||
command = command.replace('{%s}' % name, str(value))
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def execute_hook(commands, umask, config_filename, description, dry_run, **context):
|
||||
'''
|
||||
Given a list of hook commands to execute, a umask to execute with (or None), a config filename,
|
||||
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.
|
||||
|
||||
The context contains optional values interpolated by name into the hook commands. Currently,
|
||||
this only applies to the on_error hook.
|
||||
|
||||
Raise ValueError if the umask cannot be parsed.
|
||||
Raise subprocesses.CalledProcessError if an error occurs in a hook.
|
||||
'''
|
||||
if not commands:
|
||||
logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
|
||||
|
|
@ -20,6 +35,9 @@ def execute_hook(commands, umask, config_filename, description, dry_run):
|
|||
|
||||
dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
|
||||
|
||||
context['configuration_filename'] = config_filename
|
||||
commands = [interpolate_context(command, context) for command in commands]
|
||||
|
||||
if len(commands) == 1:
|
||||
logger.info(
|
||||
'{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
|
||||
36
borgmatic/hooks/healthchecks.py
Normal file
36
borgmatic/hooks/healthchecks.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def ping_healthchecks(ping_url_or_uuid, config_filename, dry_run, append=None):
|
||||
'''
|
||||
Ping the given healthchecks.io URL or UUID, appending the append string if any. Use the given
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually ping
|
||||
anything.
|
||||
'''
|
||||
if not ping_url_or_uuid:
|
||||
logger.debug('{}: No healthchecks hook set'.format(config_filename))
|
||||
return
|
||||
|
||||
ping_url = (
|
||||
ping_url_or_uuid
|
||||
if ping_url_or_uuid.startswith('http')
|
||||
else 'https://hc-ping.com/{}'.format(ping_url_or_uuid)
|
||||
)
|
||||
dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
|
||||
if append:
|
||||
ping_url = '{}/{}'.format(ping_url, append)
|
||||
|
||||
logger.info(
|
||||
'{}: Pinging healthchecks.io{}{}'.format(
|
||||
config_filename, ' ' + append if append else '', dry_run_label
|
||||
)
|
||||
)
|
||||
logger.debug('{}: Using healthchecks.io ping URL {}'.format(config_filename, ping_url))
|
||||
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
requests.get(ping_url)
|
||||
88
borgmatic/hooks/postgresql.py
Normal file
88
borgmatic/hooks/postgresql.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from borgmatic.execute import execute_command
|
||||
|
||||
DUMP_PATH = '~/.borgmatic/postgresql_databases'
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def dump_databases(databases, config_filename, dry_run):
|
||||
'''
|
||||
Dump the given PostgreSQL databases to disk. The databases are supplied as a sequence of dicts,
|
||||
one dict describing each database as per the configuration schema. Use the given configuration
|
||||
filename in any log entries. If this is a dry run, then don't actually dump anything.
|
||||
'''
|
||||
if not databases:
|
||||
logger.debug('{}: No PostgreSQL databases configured'.format(config_filename))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
|
||||
logger.info('{}: Dumping PostgreSQL databases{}'.format(config_filename, dry_run_label))
|
||||
|
||||
for database in databases:
|
||||
if os.path.sep in database['name']:
|
||||
raise ValueError('Invalid database name {}'.format(database['name']))
|
||||
|
||||
dump_path = os.path.join(
|
||||
os.path.expanduser(DUMP_PATH), database.get('hostname', 'localhost')
|
||||
)
|
||||
name = database['name']
|
||||
all_databases = bool(name == 'all')
|
||||
command = (
|
||||
('pg_dumpall' if all_databases else 'pg_dump', '--no-password', '--clean')
|
||||
+ ('--file', os.path.join(dump_path, name))
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--username', database['username']) if 'username' in database else ())
|
||||
+ (() if all_databases else ('--format', database.get('format', 'custom')))
|
||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||
+ (() if all_databases else (name,))
|
||||
)
|
||||
extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None
|
||||
|
||||
logger.debug(
|
||||
'{}: Dumping PostgreSQL database {}{}'.format(config_filename, name, dry_run_label)
|
||||
)
|
||||
if not dry_run:
|
||||
os.makedirs(dump_path, mode=0o700, exist_ok=True)
|
||||
execute_command(command, extra_environment=extra_environment)
|
||||
|
||||
|
||||
def remove_database_dumps(databases, config_filename, dry_run):
|
||||
'''
|
||||
Remove the database dumps for the given databases. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
configuration filename in any log entries. If this is a dry run, then don't actually remove
|
||||
anything.
|
||||
'''
|
||||
if not databases:
|
||||
logger.debug('{}: No PostgreSQL databases configured'.format(config_filename))
|
||||
return
|
||||
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
logger.info('{}: Removing PostgreSQL database dumps{}'.format(config_filename, dry_run_label))
|
||||
|
||||
for database in databases:
|
||||
if os.path.sep in database['name']:
|
||||
raise ValueError('Invalid database name {}'.format(database['name']))
|
||||
|
||||
name = database['name']
|
||||
dump_path = os.path.join(
|
||||
os.path.expanduser(DUMP_PATH), database.get('hostname', 'localhost')
|
||||
)
|
||||
dump_filename = os.path.join(dump_path, name)
|
||||
|
||||
logger.debug(
|
||||
'{}: Remove PostgreSQL database dump {} from {}{}'.format(
|
||||
config_filename, name, dump_filename, dry_run_label
|
||||
)
|
||||
)
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
os.remove(dump_filename)
|
||||
if len(os.listdir(dump_path)) == 0:
|
||||
os.rmdir(dump_path)
|
||||
|
|
@ -21,6 +21,14 @@ def to_bool(arg):
|
|||
return False
|
||||
|
||||
|
||||
def interactive_console():
|
||||
'''
|
||||
Return whether the current console is "interactive". Meaning: Capable of
|
||||
user input and not just something like a cron job.
|
||||
'''
|
||||
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
|
||||
|
||||
|
||||
def should_do_markup(no_color, configs):
|
||||
'''
|
||||
Given the value of the command-line no-color argument, and a dict of configuration filename to
|
||||
|
|
@ -37,7 +45,7 @@ def should_do_markup(no_color, configs):
|
|||
if py_colors is not None:
|
||||
return to_bool(py_colors)
|
||||
|
||||
return sys.stdout.isatty() and os.environ.get('TERM') != 'dumb'
|
||||
return interactive_console()
|
||||
|
||||
|
||||
LOG_LEVEL_TO_COLOR = {
|
||||
|
|
@ -82,9 +90,9 @@ def configure_logging(console_log_level, syslog_log_level=None):
|
|||
elif os.path.exists('/var/run/syslog'):
|
||||
syslog_path = '/var/run/syslog'
|
||||
|
||||
if syslog_path:
|
||||
if syslog_path and not interactive_console():
|
||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s \ufeff%(message)s'))
|
||||
syslog_handler.setFormatter(logging.Formatter('borgmatic: %(levelname)s %(message)s'))
|
||||
syslog_handler.setLevel(syslog_log_level)
|
||||
handlers = (console_handler, syslog_handler)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.7.3-alpine3.9 as borgmatic
|
||||
FROM python:3.7.4-alpine3.10 as borgmatic
|
||||
|
||||
COPY . /app
|
||||
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
|
||||
|
|
@ -7,7 +7,9 @@ RUN borgmatic --help > /command-line.txt \
|
|||
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
|
||||
&& borgmatic "$action" --help >> /command-line.txt; done
|
||||
|
||||
FROM node:11.15.0-alpine as html
|
||||
FROM node:12.10.0-alpine as html
|
||||
|
||||
ARG ENVIRONMENT=production
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
|
|
@ -20,10 +22,10 @@ RUN npm install @11ty/eleventy \
|
|||
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 \
|
||||
RUN NODE_ENV=${ENVIRONMENT} npx eleventy --input=/source/docs --output=/output/docs \
|
||||
&& mv /output/docs/index.html /output/index.html
|
||||
|
||||
FROM nginx:1.16.0-alpine
|
||||
FROM nginx:1.16.1-alpine
|
||||
|
||||
COPY --from=html /output /usr/share/nginx/html
|
||||
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
|
||||
|
|
|
|||
18
docs/_includes/components/suggestion-form.css
Normal file
18
docs/_includes/components/suggestion-form.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#suggestion-form textarea {
|
||||
font-family: sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#suggestion-form label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#suggestion-form input[type=email] {
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#suggestion-form .form-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
33
docs/_includes/components/suggestion-form.html
Normal file
33
docs/_includes/components/suggestion-form.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<h2>Improve this documentation</h2>
|
||||
|
||||
<p>Have an idea on how to make this documentation even better? Send your
|
||||
feedback below! (But if you need help installing or using borgmatic, please
|
||||
use our <a href="https://torsion.org/borgmatic/#issues">issue tracker</a>
|
||||
instead.)</p>
|
||||
|
||||
<form id="suggestion-form">
|
||||
<div><label for="suggestion">Suggestion</label></div>
|
||||
<textarea id="suggestion" rows="8" cols="60" name="suggestion"></textarea>
|
||||
<div data-sk-error="suggestion" class="form-error"></div>
|
||||
<input id="_page" type="hidden" name="_page">
|
||||
<input id="_subject" type="hidden" name="_subject" value="borgmatic documentation suggestion">
|
||||
<br />
|
||||
<label for="email">Email address</label>
|
||||
<div><input id="email" type="email" name="email" placeholder="Only required if you want a response!"></div>
|
||||
<div data-sk-error="email" class="form-error"></div>
|
||||
<br />
|
||||
<div><button type="submit">Send</button></div>
|
||||
<br />
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('_page').value = window.location.href;
|
||||
window.sk=window.sk||function(){(sk.q=sk.q||[]).push(arguments)};
|
||||
|
||||
sk('form', 'init', {
|
||||
id: '1d536680ab96',
|
||||
element: '#suggestion-form'
|
||||
});
|
||||
</script>
|
||||
|
||||
<script defer src="https://js.statickit.com/statickit.js"></script>
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
{% include 'components/minilink.css' %}
|
||||
{% include 'components/toc.css' %}
|
||||
{% include 'components/info-blocks.css' %}
|
||||
{% include 'components/suggestion-form.css' %}
|
||||
{% include 'prism-theme.css' %}
|
||||
{% include 'asciinema.css' %}
|
||||
{% endset %}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,7 @@ headerClass: elv-header-default
|
|||
<main class="elv-layout{% if layoutClass %} {{ layoutClass }}{% endif %}">
|
||||
<article>
|
||||
{{ content | safe }}
|
||||
|
||||
{% include 'components/suggestion-form.html' %}
|
||||
</article>
|
||||
</main>
|
||||
|
|
|
|||
79
docs/how-to/add-preparation-and-cleanup-steps-to-backups.md
Normal file
79
docs/how-to/add-preparation-and-cleanup-steps-to-backups.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
---
|
||||
title: How to add preparation and cleanup steps to backups
|
||||
---
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If you find yourself performing prepraration tasks before your backup runs, or
|
||||
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are shell
|
||||
commands that borgmatic executes for you at various points, and they're
|
||||
configured in the `hooks` section of your configuration file. But if you're
|
||||
looking to backup a database, it's probably easier to use the [database backup
|
||||
feature](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
instead.
|
||||
|
||||
You can specify `before_backup` hooks to perform preparation steps before
|
||||
running backups, and specify `after_backup` hooks to perform cleanup steps
|
||||
afterwards. Here's an example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
before_backup:
|
||||
- mount /some/filesystem
|
||||
after_backup:
|
||||
- umount /some/filesystem
|
||||
```
|
||||
|
||||
The `before_backup` and `after_backup` hooks each run once per configuration
|
||||
file. `before_backup` hooks run prior to backups of all repositories in a
|
||||
configuration file, right before the `create` action. `after_backup` hooks run
|
||||
afterwards, but not if an error occurs in a previous hook or in the backups
|
||||
themselves.
|
||||
|
||||
You can also use `before_everything` and `after_everything` hooks to perform
|
||||
global setup or cleanup:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
before_everything:
|
||||
- set-up-stuff-globally
|
||||
after_everything:
|
||||
- clean-up-stuff-globally
|
||||
```
|
||||
|
||||
`before_everything` hooks collected from all borgmatic configuration files run
|
||||
once before all configuration files (prior to all actions), but only if there
|
||||
is a `create` action. An error encountered during a `before_everything` hook
|
||||
causes borgmatic to exit without creating backups.
|
||||
|
||||
`after_everything` hooks run once after all configuration files and actions,
|
||||
but only if there is a `create` action. It runs even if an error occurs during
|
||||
a backup or a backup hook, but not if an error occurs during a
|
||||
`before_everything` hook.
|
||||
|
||||
borgmatic also runs `on_error` hooks if an error occurs, either when creating
|
||||
a backup or running a backup hook. See the [monitoring and alerting
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
for more information.
|
||||
|
||||
## Hook output
|
||||
|
||||
Any output produced by your hooks shows up both at the console and in syslog
|
||||
(when run in a non-interactive console). For more information, read about <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/">inspecting
|
||||
your backups</a>.
|
||||
|
||||
## Security
|
||||
|
||||
An important security note about hooks: borgmatic executes all hook commands
|
||||
with the user permissions of borgmatic itself. So to prevent potential shell
|
||||
injection or privilege escalation, do not forget to set secure permissions
|
||||
on borgmatic configuration files (`chmod 0600`) and scripts (`chmod 0700`)
|
||||
invoked by hooks.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Backup your databases](https://torsion.org/borgmatic/docs/how-to/backup-your-databases/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
81
docs/how-to/backup-your-databases.md
Normal file
81
docs/how-to/backup-your-databases.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
title: How to backup your databases
|
||||
---
|
||||
## Database dump hooks
|
||||
|
||||
If you want to backup a database, it's best practice with most database
|
||||
systems to backup an exported database dump, rather than backing up your
|
||||
database's internal file storage. That's because the internal storage can
|
||||
change while you're reading from it. In contrast, a database dump creates a
|
||||
consistent snapshot that is more suited for backups.
|
||||
|
||||
Fortunately, borgmatic includes built-in support for creating database dumps
|
||||
prior to running backups. For example, here is everything you need to dump and
|
||||
backup a couple of local PostgreSQL databases:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
- name: orders
|
||||
```
|
||||
|
||||
Prior to each backup, borgmatic dumps each configured database to a file
|
||||
(located in `~/.borgmatic/`) and includes it in the backup. After the backup
|
||||
completes, borgmatic removes the database dump files to recover disk space.
|
||||
|
||||
Here's a more involved example that connects to a remote database:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: database.example.org
|
||||
port: 5433
|
||||
username: dbuser
|
||||
password: trustsome1
|
||||
format: tar
|
||||
options: "--role=someone"
|
||||
```
|
||||
|
||||
If you want to dump all databases on a host, use `all` for the database name:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
postgresql_databases:
|
||||
- name: all
|
||||
```
|
||||
|
||||
Note that you may need to use a `username` of the `postgres` superuser for
|
||||
this to work.
|
||||
|
||||
## Supported databases
|
||||
|
||||
As of now, borgmatic only supports PostgreSQL databases directly. But see
|
||||
below about general-purpose preparation and cleanup hooks as a work-around
|
||||
with other database systems. Also, please [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) for additional database systems
|
||||
that you'd like supported.
|
||||
|
||||
## Database restoration
|
||||
|
||||
borgmatic does not yet perform integrated database restoration when you
|
||||
[restore a backup](http://localhost:8080/docs/how-to/restore-a-backup/), but
|
||||
that feature is coming in a future release. In the meantime, you can restore
|
||||
a database manually after restoring a dump file in the `~/.borgmatic` path.
|
||||
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If this database integration is too limited for needs, borgmatic also supports
|
||||
general-purpose [preparation and cleanup
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/set-up-backups/). These
|
||||
hooks allows you to trigger arbitrary commands or scripts before and after
|
||||
backups. So if necessary, you can use these hooks to create database dumps
|
||||
with any database system.
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Restore a backup](http://localhost:8080/docs/how-to/restore-a-backup/)
|
||||
|
|
@ -14,7 +14,7 @@ repositories.
|
|||
|
||||
If you find yourself in this situation, you have some options. First, you can
|
||||
run borgmatic's pruning, creating, or checking actions separately. For
|
||||
instance, the the following optional flags are available:
|
||||
instance, the the following optional actions are available:
|
||||
|
||||
```bash
|
||||
borgmatic prune
|
||||
|
|
@ -22,7 +22,10 @@ borgmatic create
|
|||
borgmatic check
|
||||
```
|
||||
|
||||
You can run with only one of these flags provided, or you can mix and match
|
||||
(No borgmatic `prune`, `create`, or `check` actions? Try the old-style
|
||||
`--prune`, `--create`, or `--check`. Or upgrade borgmatic!)
|
||||
|
||||
You can run with only one of these actions provided, or you can mix and match
|
||||
any number of them in a single borgmatic run. This supports approaches like
|
||||
making backups with `create` on a frequent schedule, while only running
|
||||
expensive consistency checks with `check` on a much less frequent basis from
|
||||
|
|
@ -65,6 +68,16 @@ consistency:
|
|||
- path/of/repository_to_check.borg
|
||||
```
|
||||
|
||||
Finally, you can override your configuration file's consistency checks, and
|
||||
run particular checks via the command-line. For instance:
|
||||
|
||||
```bash
|
||||
borgmatic check --only data --only extract
|
||||
```
|
||||
|
||||
This is useful for running slow consistency checks on an infrequent basis,
|
||||
separate from your regular checks.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -93,4 +106,4 @@ backups.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
|
|
|||
|
|
@ -109,4 +109,4 @@ also linked from the commits list on each pull request.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,19 @@ Or, for even more progress and debug spew:
|
|||
borgmatic --verbosity 2
|
||||
```
|
||||
|
||||
## Backup summary
|
||||
|
||||
If you're less concerned with progress during a backup, and you only want to
|
||||
see the summary of archive statistics at the end, you can use the stats
|
||||
option when performing a backup:
|
||||
|
||||
```bash
|
||||
borgmatic --stats
|
||||
```
|
||||
|
||||
## Existing backups
|
||||
|
||||
Borgmatic provides convenient flags for Borg's
|
||||
borgmatic provides convenient actions for Borg's
|
||||
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
|
||||
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
||||
functionality:
|
||||
|
|
@ -33,12 +43,17 @@ borgmatic list
|
|||
borgmatic info
|
||||
```
|
||||
|
||||
(No borgmatic `list` or `info` actions? Try the old-style `--list` or
|
||||
`--info`. Or upgrade borgmatic!)
|
||||
|
||||
|
||||
## 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.
|
||||
present and borgmatic is running in a non-interactive console. 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
|
||||
|
|
@ -68,18 +83,10 @@ 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](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)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
|
|
|||
|
|
@ -112,4 +112,4 @@ directly, please see the section above about standard includes.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
|
|
|
|||
158
docs/how-to/monitor-your-backups.md
Normal file
158
docs/how-to/monitor-your-backups.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
---
|
||||
title: How to monitor your backups
|
||||
---
|
||||
|
||||
## Monitoring and alerting
|
||||
|
||||
Having backups is great, but they won't do you a lot of good unless you have
|
||||
confidence that they're running on a regular basis. That's where monitoring
|
||||
and alerting comes in.
|
||||
|
||||
There are several different ways you can monitor your backups and find out
|
||||
whether they're succeeding. Which of these you choose to do is up to you and
|
||||
your particular infrastructure:
|
||||
|
||||
1. **Job runner alerts**: The easiest place to start is with failure alerts
|
||||
from the [scheduled job
|
||||
runner](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot) (cron,
|
||||
systemd, etc.) that's running borgmatic. But note that if the job doesn't even
|
||||
get scheduled (e.g. due to the job runner not running), you probably won't get
|
||||
an alert at all! Still, this is a decent first line of defense, especially
|
||||
when combined with some of the other approaches below.
|
||||
2. **borgmatic error hooks**: The `on_error` hook allows you to run an arbitrary
|
||||
command or script when borgmatic itself encounters an error running your
|
||||
backups. So for instance, you can run a script to send yourself a text message
|
||||
alert. But note that if borgmatic doesn't actually run, this alert won't fire.
|
||||
See [error
|
||||
hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#error-hooks)
|
||||
below for how to configure this.
|
||||
4. **borgmatic Healthchecks hook**: This feature integrates with the
|
||||
[Healthchecks](https://healthchecks.io/) service, and pings Healthchecks
|
||||
whenever borgmatic runs. That way, Healthchecks can alert you when something
|
||||
goes wrong or it doesn't hear from borgmatic for a configured interval. See
|
||||
[Healthchecks
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
below for how to configure this.
|
||||
3. **Third-party monitoring software**: You can use traditional monitoring
|
||||
software to consume borgmatic JSON output and track when the last
|
||||
successful backup occurred. See [scripting
|
||||
borgmatic](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#scripting-borgmatic)
|
||||
below for how to configure this.
|
||||
5. **Borg hosting providers**: Most [Borg hosting
|
||||
providers](https://torsion.org/borgmatic/#hosting-providers) include
|
||||
monitoring and alerting as part of their offering. This gives you a dashboard
|
||||
to check on all of your backups, and can alert you if the service doesn't hear
|
||||
from borgmatic for a configured interval.
|
||||
6. **borgmatic consistency checks**: While not strictly part of monitoring, if you
|
||||
really want confidence that your backups are not only running but are
|
||||
restorable as well, you can configure particular [consistency
|
||||
checks](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration)
|
||||
or even script full [restore
|
||||
tests](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/).
|
||||
|
||||
|
||||
## Error hooks
|
||||
|
||||
When an error occurs during a backup, borgmatic can run configurable shell
|
||||
commands to fire off custom error notifications or take other actions, so you
|
||||
can get alerted as soon as something goes wrong. Here's a not-so-useful
|
||||
example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- echo "Error while creating a backup or running a backup hook."
|
||||
```
|
||||
|
||||
The `on_error` hook supports interpolating particular runtime variables into
|
||||
the hook command. Here's an example that assumes you provide a separate shell
|
||||
script to handle the alerting:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- send-text-message.sh "{configuration_filename}" "{repository}"
|
||||
```
|
||||
|
||||
In this example, when the error occurs, borgmatic interpolates a few runtime
|
||||
values into the hook command: the borgmatic configuration filename, and the
|
||||
path of the repository. Here's the full set of supported variables you can use
|
||||
here:
|
||||
|
||||
* `configuration_filename`: borgmatic configuration filename in which the
|
||||
error occurred
|
||||
* `repository`: path of the repository in which the error occurred (may be
|
||||
blank if the error occurs in a hook)
|
||||
* `error`: the error message itself
|
||||
* `output`: output of the command that failed (may be blank if an error
|
||||
occurred without running a command)
|
||||
|
||||
Note that borgmatic does not run `on_error` hooks if an error occurs within a
|
||||
`before_everything` or `after_everything` hook. For more about hooks, see the
|
||||
[borgmatic hooks
|
||||
documentation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/),
|
||||
especially the security information.
|
||||
|
||||
|
||||
## Healthchecks hook
|
||||
|
||||
[Healthchecks](https://healthchecks.io/) is a service that provides "instant
|
||||
alerts when your cron jobs fail silently", and borgmatic has built-in
|
||||
integration with it. Once you create a Healthchecks account and project on
|
||||
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||
URL" for your project. Here's an example:
|
||||
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
healthchecks: https://hc-ping.com/addffa72-da17-40ae-be9c-ff591afb942a
|
||||
```
|
||||
|
||||
With this hook in place, borgmatic will ping your Healthchecks project when a
|
||||
backup begins, ends, or errors. Then you can configure Healthchecks to notify
|
||||
you by a [variety of
|
||||
mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
|
||||
or it doesn't hear from borgmatic for a certain period of time.
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
optional `--json` flag with `create`, `list`, or `info` to get the output
|
||||
formatted as JSON.
|
||||
|
||||
Note that when you specify the `--json` flag, Borg's other non-JSON output is
|
||||
suppressed so as not to interfere with the captured JSON. Also note that JSON
|
||||
output only shows up at the console, and not in syslog.
|
||||
|
||||
|
||||
### Successful backups
|
||||
|
||||
`borgmatic list` includes support for a `--successful` flag that only lists
|
||||
successful (non-checkpoint) backups. This flag works via a basic heuristic: It
|
||||
assumes that non-checkpoint archive names end with a digit (e.g. from a
|
||||
timestamp), while checkpoint archive names do not. This means that if you're
|
||||
using custom archive names that do not end in a digit, the `--successful` flag
|
||||
will not work as expected.
|
||||
|
||||
Combined with a built-in Borg flag like `--last`, you can list the last
|
||||
successful backup for use in your monitoring scripts. Here's an example
|
||||
combined with `--json`:
|
||||
|
||||
```bash
|
||||
borgmatic list --successful --last 1 --json
|
||||
```
|
||||
|
||||
Note that this particular combination will only work if you've got a single
|
||||
backup "series" in your repository. If you're instead backing up, say, from
|
||||
multiple different hosts into a single repository, then you'll need to get
|
||||
fancier with your archive listing. See `borg list --help` for more flags.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Add preparation and cleanup steps to backups](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/)
|
||||
* [Restore a backup](https://torsion.org/borgmatic/docs/how-to/restore-a-backup/)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
|
@ -11,6 +11,9 @@ to figure out which archive to restore. A good way to do that is to use the
|
|||
borgmatic list
|
||||
```
|
||||
|
||||
(No borgmatic `list` action? Try the old-style `--list`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
That should yield output looking something like:
|
||||
|
||||
```text
|
||||
|
|
@ -25,6 +28,9 @@ and therefore the latest timestamp, run a command like:
|
|||
borgmatic extract --archive host-2019-01-02T04:06:07.080910
|
||||
```
|
||||
|
||||
(No borgmatic `extract` action? Try the old-style `--extract`, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
The `--archive` value is the name of the archive to restore. This extracts the
|
||||
entire contents of the archive to the current directory, so make sure you're
|
||||
in the right place before running the command.
|
||||
|
|
@ -57,5 +63,6 @@ Like a whole-archive restore, this also restores into the current directory.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups.md)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups.md)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
|
|
|
|||
|
|
@ -1,58 +1,3 @@
|
|||
---
|
||||
title: How to run preparation steps before backups
|
||||
---
|
||||
## Preparation and cleanup hooks
|
||||
|
||||
If you find yourself performing prepraration tasks before your backup runs, or
|
||||
cleanup work afterwards, borgmatic hooks may be of interest. Hooks are
|
||||
shell commands that borgmatic executes for you at various points, and they're
|
||||
configured in the `hooks` section of your configuration file.
|
||||
|
||||
For instance, you can specify `before_backup` hooks to dump a database to file
|
||||
before backing it up, and specify `after_backup` hooks to delete the temporary
|
||||
file afterwards. Here's an example:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
before_backup:
|
||||
- dump-a-database /to/file.sql
|
||||
after_backup:
|
||||
- rm /to/file.sql
|
||||
```
|
||||
|
||||
borgmatic hooks run once per configuration file. `before_backup` hooks run
|
||||
prior to backups of all repositories. `after_backup` hooks run afterwards, but
|
||||
not if an error occurs in a previous hook or in the backups themselves.
|
||||
|
||||
|
||||
## Error hooks
|
||||
|
||||
borgmatic also runs `on_error` hooks if an error occurs. Here's an example
|
||||
configuration:
|
||||
|
||||
```yaml
|
||||
hooks:
|
||||
on_error:
|
||||
- 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
|
||||
|
||||
An important security note about hooks: borgmatic executes all hook commands
|
||||
with the user permissions of borgmatic itself. So to prevent potential shell
|
||||
injection or privilege escalation, do not forget to set secure permissions
|
||||
(`chmod 0700`) on borgmatic configuration files and scripts invoked by hooks.
|
||||
|
||||
|
||||
## Related documentation
|
||||
|
||||
* [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)
|
||||
<head>
|
||||
<meta http-equiv='refresh' content='0; URL=https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/'>
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ title: How to set up backups with borgmatic
|
|||
## Installation
|
||||
|
||||
To get up and running, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at
|
||||
least version 1.1.
|
||||
|
||||
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.
|
||||
By default, borgmatic looks for its configuration files in `/etc/borgmatic/`
|
||||
and `/etc/borgmatic.d/`, where the root user typically has read access.
|
||||
|
||||
Run the following commands to download and install borgmatic:
|
||||
So, to download and install borgmatic as the root user, run the following
|
||||
commands:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --user --upgrade borgmatic
|
||||
|
|
@ -35,9 +35,11 @@ borgmatic:
|
|||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Arch Linux](https://aur.archlinux.org/packages/borgmatic/)
|
||||
* [Arch Linux](https://www.archlinux.org/packages/community/any/borgmatic/)
|
||||
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||
* [openSUSE](https://software.opensuse.org/package/borgmatic)
|
||||
* [stand-alone binary](https://github.com/cmarquardt/borgmatic-binary)
|
||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/)
|
||||
|
||||
|
||||
## Hosting providers
|
||||
|
|
@ -71,10 +73,11 @@ to ignore anything you don't need.
|
|||
Note that the configuration file is organized into distinct sections, each
|
||||
with a section name like `location:` or `storage:`. So take care that if you
|
||||
uncomment a particular option, also uncomment its containing section name, or
|
||||
else borgmatic won't recognize the option.
|
||||
else borgmatic won't recognize the option. Also be sure to use spaces rather
|
||||
than tabs for indentation; YAML does not allow tabs.
|
||||
|
||||
You can also get the same sample configuration file from the [configuration
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration.md), the authoritative set of
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/), the authoritative set of
|
||||
all configuration options. This is handy if borgmatic has added new options
|
||||
since you originally created your configuration file.
|
||||
|
||||
|
|
@ -86,7 +89,7 @@ encrypt your Borg repository with a passphrase instead of a key file, you'll
|
|||
either need to set the borgmatic `encryption_passphrase` configuration
|
||||
variable or set the `BORG_PASSPHRASE` environment variable. See the
|
||||
[repository encryption
|
||||
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
|
||||
section](https://borgbackup.readthedocs.io/en/stable/quickstart.html#repository-encryption)
|
||||
of the Borg Quick Start for more info.
|
||||
|
||||
Alternatively, you can specify the passphrase programatically by setting
|
||||
|
|
@ -124,15 +127,18 @@ a command like the following:
|
|||
borgmatic init --encryption repokey
|
||||
```
|
||||
|
||||
(No borgmatic `init` action? Try the old-style `--init` flag, or upgrade
|
||||
borgmatic!)
|
||||
|
||||
This uses the borgmatic configuration file you created above to determine
|
||||
which local or remote repository to create, and encrypts it with the
|
||||
encryption passphrase specified there if one is provided. Read about [Borg
|
||||
encryption
|
||||
modes](https://borgbackup.readthedocs.io/en/latest/usage/init.html#encryption-modes)
|
||||
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-modes)
|
||||
for the menu of available encryption modes.
|
||||
|
||||
Also, optionally check out the [Borg Quick
|
||||
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more
|
||||
Start](https://borgbackup.readthedocs.org/en/stable/quickstart.html) for more
|
||||
background about repository initialization.
|
||||
|
||||
Note that borgmatic skips repository initialization if the repository already
|
||||
|
|
@ -209,6 +215,22 @@ section of configuration.
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
### "found character that cannot start any token" error
|
||||
|
||||
If you run borgmatic and see an error looking something like this, it probably
|
||||
means you've used tabs instead of spaces:
|
||||
|
||||
```
|
||||
test.yaml: Error parsing configuration file
|
||||
An error occurred while parsing a configuration file at config.yaml:
|
||||
while scanning for the next token
|
||||
found character that cannot start any token
|
||||
in "config.yaml", line 230, column 1
|
||||
```
|
||||
|
||||
YAML does not allow tabs. So to fix this, replace any tabs in your
|
||||
configuration file with the requisite number of spaces.
|
||||
|
||||
### libyaml compilation errors
|
||||
|
||||
borgmatic depends on a Python YAML library (ruamel.yaml) that will optionally
|
||||
|
|
@ -222,13 +244,9 @@ it.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [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>
|
||||
* [Make per-application backups](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/)
|
||||
* [Deal with very large backups](https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/)
|
||||
* [Inspect your backups](https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/)
|
||||
* [Monitor your backups](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||
|
|
|
|||
|
|
@ -76,4 +76,4 @@ files.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic.md)
|
||||
* [Develop on borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/)
|
||||
|
|
|
|||
|
|
@ -13,5 +13,5 @@ each action sub-command:
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [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)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [borgmatic configuration reference](https://torsion.org/borgmatic/docs/reference/configuration/)
|
||||
|
|
|
|||
|
|
@ -15,5 +15,5 @@ file](https://torsion.org/borgmatic/docs/reference/config.yaml) for use locally.
|
|||
|
||||
## Related documentation
|
||||
|
||||
* [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)
|
||||
* [Set up backups with borgmatic](https://torsion.org/borgmatic/docs/how-to/set-up-backups/)
|
||||
* [borgmatic command-line reference](https://torsion.org/borgmatic/docs/reference/command-line/)
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
||||
|
||||
0 3 * * * PATH=$PATH:/usr/bin /root/.local/bin/borgmatic
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# You can drop this file into /etc/cron.d/ to run borgmatic nightly.
|
||||
|
||||
0 3 * * * root PATH=$PATH:/usr/local/bin /root/.local/bin/borgmatic
|
||||
0 3 * * * root PATH=$PATH:/usr/bin:/usr/local/bin /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||
|
|
|
|||
|
|
@ -1,7 +1,22 @@
|
|||
[Unit]
|
||||
Description=borgmatic backup
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
ConditionACPower=true
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/.local/bin/borgmatic
|
||||
|
||||
# Lower CPU and I/O priority.
|
||||
Nice=19
|
||||
CPUSchedulingPolicy=batch
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=7
|
||||
IOWeight=100
|
||||
|
||||
Restart=no
|
||||
LogRateLimitIntervalSec=0
|
||||
|
||||
# Delay start to prevent backups running during boot.
|
||||
ExecStartPre=sleep 1m
|
||||
ExecStart=systemd-inhibit --who="borgmatic" --why="Prevent interrupting scheduled backup" /root/.local/bin/borgmatic --syslog-verbosity 1
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
set -e
|
||||
|
||||
docker build --tag borgmatic-docs --file docs/Dockerfile .
|
||||
docker build --tag borgmatic-docs --build-arg ENVIRONMENT=dev --file docs/Dockerfile .
|
||||
echo
|
||||
echo "You can view dev docs at http://localhost:8080"
|
||||
echo "Note that links within these docs will go to the online docs, so you will need to fiddle with URLs manually to stay in the dev docs."
|
||||
echo
|
||||
docker run --interactive --tty --publish 8080:80 --rm borgmatic-docs
|
||||
|
|
|
|||
|
|
@ -48,6 +48,17 @@ for sub_command in prune create check list info; do
|
|||
| grep -v '^--stats$' \
|
||||
| grep -v '^--verbose$' \
|
||||
| grep -v '^--warning$' \
|
||||
| grep -v '^--exclude' \
|
||||
| grep -v '^--exclude-from' \
|
||||
| grep -v '^--first' \
|
||||
| grep -v '^--format' \
|
||||
| grep -v '^--glob-archives' \
|
||||
| grep -v '^--last' \
|
||||
| grep -v '^--list-format' \
|
||||
| grep -v '^--patterns-from' \
|
||||
| grep -v '^--prefix' \
|
||||
| grep -v '^--short' \
|
||||
| grep -v '^--sort-by' \
|
||||
| grep -v '^-h$' \
|
||||
>> all_borg_flags
|
||||
done
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
set -e
|
||||
|
||||
python -m pip install --upgrade pip==19.1.1
|
||||
pip install tox==3.10.0
|
||||
pip install tox==3.14.0
|
||||
tox
|
||||
apk add --no-cache borgbackup
|
||||
tox -e end-to-end
|
||||
|
|
|
|||
5
setup.py
5
setup.py
|
|
@ -1,6 +1,6 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.3.9'
|
||||
VERSION = '1.4.0'
|
||||
|
||||
|
||||
setup(
|
||||
|
|
@ -31,7 +31,8 @@ setup(
|
|||
obsoletes=['atticmatic'],
|
||||
install_requires=(
|
||||
'pykwalify>=1.6.0,<14.06',
|
||||
'ruamel.yaml>0.15.0,<0.16.0',
|
||||
'requests',
|
||||
'ruamel.yaml>0.15.0,<0.17.0',
|
||||
'setuptools',
|
||||
'colorama>=0.4.1,<0.5',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,21 +4,22 @@ attrs==19.1.0
|
|||
black==19.3b0; python_version >= '3.6'
|
||||
click==7.0
|
||||
colorama==0.4.1
|
||||
coverage==4.5.3
|
||||
coverage==4.5.4
|
||||
docopt==0.6.2
|
||||
flake8==3.7.7
|
||||
flake8==3.7.8
|
||||
flexmock==0.10.4
|
||||
isort==4.3.20
|
||||
isort==4.3.21
|
||||
mccabe==0.6.1
|
||||
more-itertools==7.0.0
|
||||
pluggy==0.12.0
|
||||
more-itertools==7.2.0
|
||||
pluggy==0.13.0
|
||||
py==1.8.0
|
||||
pycodestyle==2.5.0
|
||||
pyflakes==2.1.1
|
||||
pykwalify==1.7.0
|
||||
pytest==4.6.3
|
||||
pytest==5.1.2
|
||||
pytest-cov==2.7.1
|
||||
python-dateutil==2.8.0
|
||||
PyYAML==5.1.1
|
||||
ruamel.yaml>0.15.0,<0.16.0
|
||||
PyYAML==5.1.2
|
||||
requests==2.22.0
|
||||
ruamel.yaml>0.15.0,<0.17.0
|
||||
toml==0.10.0
|
||||
|
|
|
|||
|
|
@ -78,6 +78,18 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
|
|||
assert 'check' in arguments
|
||||
|
||||
|
||||
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
arguments = module.parse_arguments('--stats')
|
||||
|
||||
assert 'prune' in arguments
|
||||
assert arguments['prune'].stats
|
||||
assert 'create' in arguments
|
||||
assert arguments['create'].stats
|
||||
assert 'check' in arguments
|
||||
|
||||
|
||||
def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys):
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
|
@ -218,6 +230,15 @@ def test_parse_arguments_disallows_init_and_dry_run():
|
|||
)
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_glob_archives_with_successful():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments(
|
||||
'--config', 'myconfig', 'list', '--glob-archives', '*glob*', '--successful'
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_repository_without_extract_or_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
|
@ -322,12 +343,6 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
|
|||
module.parse_arguments('--stats', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_just_stats_flag_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--stats')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_json_with_list_or_info():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
|
|
@ -346,3 +361,21 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
|
|||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments('list', 'info', '--json')
|
||||
|
||||
|
||||
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('check', '--only', 'extract')
|
||||
|
||||
|
||||
def test_parse_arguments_extract_archive_check_does_not_raise_check_subparser_error():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('extract', '--archive', 'check')
|
||||
|
||||
|
||||
def test_parse_arguments_extract_with_check_only_extract_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('extract', '--archive', 'name', 'check', '--only', 'extract')
|
||||
|
|
|
|||
|
|
@ -31,17 +31,23 @@ def test_comment_out_line_skips_already_commented_out_line():
|
|||
def test_comment_out_line_comments_section_name():
|
||||
line = 'figgy-pudding:'
|
||||
|
||||
assert module._comment_out_line(line) == '#' + line
|
||||
assert module._comment_out_line(line) == '# ' + line
|
||||
|
||||
|
||||
def test_comment_out_line_comments_indented_option():
|
||||
line = ' enabled: true'
|
||||
|
||||
assert module._comment_out_line(line) == ' #enabled: true'
|
||||
assert module._comment_out_line(line) == ' # enabled: true'
|
||||
|
||||
|
||||
def test_comment_out_line_comments_twice_indented_option():
|
||||
line = ' - item'
|
||||
|
||||
assert module._comment_out_line(line) == ' # - item'
|
||||
|
||||
|
||||
def test_comment_out_optional_configuration_comments_optional_config_only():
|
||||
flexmock(module)._comment_out_line = lambda line: '#' + line
|
||||
flexmock(module)._comment_out_line = lambda line: '# ' + line
|
||||
config = '''
|
||||
foo:
|
||||
bar:
|
||||
|
|
@ -56,27 +62,28 @@ location:
|
|||
other: thing
|
||||
'''
|
||||
|
||||
# flake8: noqa
|
||||
expected_config = '''
|
||||
#foo:
|
||||
# bar:
|
||||
# - baz
|
||||
# - quux
|
||||
#
|
||||
# foo:
|
||||
# bar:
|
||||
# - baz
|
||||
# - quux
|
||||
#
|
||||
location:
|
||||
repositories:
|
||||
- one
|
||||
- two
|
||||
#
|
||||
# other: thing
|
||||
#
|
||||
# other: thing
|
||||
'''
|
||||
|
||||
assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip()
|
||||
|
||||
|
||||
def test_render_configuration_does_not_raise():
|
||||
flexmock(module.yaml).should_receive('round_trip_dump')
|
||||
def test_render_configuration_converts_configuration_to_yaml_string():
|
||||
yaml_string = module._render_configuration({'foo': 'bar'})
|
||||
|
||||
module._render_configuration({})
|
||||
assert yaml_string == 'foo: bar\n'
|
||||
|
||||
|
||||
def test_write_configuration_does_not_raise():
|
||||
|
|
@ -106,12 +113,33 @@ def test_write_configuration_with_already_existing_directory_does_not_raise():
|
|||
module.write_configuration('config.yaml', 'config: yaml')
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_does_not_raise():
|
||||
def test_add_comments_to_configuration_sequence_of_strings_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq(['foo', 'bar'])
|
||||
schema = {'seq': [{'type': 'str'}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_sequence_of_maps_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
|
||||
schema = {'seq': [{'map': {'foo': {'desc': 'yo'}}}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_sequence_of_maps_without_description_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq([module.yaml.comments.CommentedMap([('foo', 'yo')])])
|
||||
schema = {'seq': [{'map': {'foo': {}}}]}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_map_does_not_raise():
|
||||
# Ensure that it can deal with fields both in the schema and missing from the schema.
|
||||
config = module.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {'map': {'foo': {'desc': 'Foo'}, 'bar': {'desc': 'Bar'}}}
|
||||
|
||||
module.add_comments_to_configuration(config, schema)
|
||||
module.add_comments_to_configuration_map(config, schema)
|
||||
|
||||
|
||||
def test_generate_sample_configuration_does_not_raise():
|
||||
|
|
|
|||
|
|
@ -7,30 +7,89 @@ from flexmock import flexmock
|
|||
from borgmatic import execute as module
|
||||
|
||||
|
||||
def test_borg_command_identifies_borg_command():
|
||||
assert module.borg_command(['/usr/bin/borg1', 'info'])
|
||||
|
||||
|
||||
def test_borg_command_does_not_identify_other_command():
|
||||
assert not module.borg_command(['grep', 'stuff'])
|
||||
|
||||
|
||||
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()
|
||||
flexmock(module).should_receive('borg_command').and_return(False)
|
||||
|
||||
module.execute_and_log_output(
|
||||
['echo', 'borg: error: oopsie'], output_log_level=logging.INFO, shell=False
|
||||
['echo', 'hi'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
module.execute_and_log_output(
|
||||
['echo', 'there'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_borg_warning_does_not_raise():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('borg_command').and_return(True)
|
||||
|
||||
module.execute_and_log_output(
|
||||
['false'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
|
||||
def test_execute_and_log_output_includes_borg_error_output_in_exception():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('borg_command').and_return(True)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(
|
||||
['grep'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
assert error.value.returncode == 2
|
||||
assert error.value.output
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_non_borg_error_raises():
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('borg_command').and_return(False)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(
|
||||
['false'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
assert error.value.returncode == 1
|
||||
|
||||
|
||||
def test_execute_and_log_output_truncates_long_borg_error_output():
|
||||
flexmock(module).ERROR_OUTPUT_MAX_LINE_COUNT = 0
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('borg_command').and_return(False)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError) as error:
|
||||
module.execute_and_log_output(
|
||||
['grep'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
assert error.value.returncode == 2
|
||||
assert error.value.output.startswith('...')
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_no_output_logs_nothing():
|
||||
flexmock(module.logger).should_receive('log').never()
|
||||
flexmock(module).should_receive('borg_command').and_return(False)
|
||||
|
||||
module.execute_and_log_output(['true'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['true'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
||||
|
||||
def test_execute_and_log_output_with_error_exit_status_raises():
|
||||
flexmock(module.logger).should_receive('log').never()
|
||||
flexmock(module.logger).should_receive('log')
|
||||
flexmock(module).should_receive('borg_command').and_return(False)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
module.execute_and_log_output(['false'], output_log_level=logging.INFO, shell=False)
|
||||
module.execute_and_log_output(
|
||||
['grep'], output_log_level=logging.INFO, shell=False, environment=None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,32 +34,76 @@ def test_parse_checks_with_blank_value_returns_defaults():
|
|||
assert checks == module.DEFAULT_CHECKS
|
||||
|
||||
|
||||
def test_parse_checks_with_none_value_returns_defaults():
|
||||
checks = module._parse_checks({'checks': None})
|
||||
|
||||
assert checks == module.DEFAULT_CHECKS
|
||||
|
||||
|
||||
def test_parse_checks_with_disabled_returns_no_checks():
|
||||
checks = module._parse_checks({'checks': ['disabled']})
|
||||
|
||||
assert checks == ()
|
||||
|
||||
|
||||
def test_parse_checks_with_data_check_also_injects_archives():
|
||||
checks = module._parse_checks({'checks': ['data']})
|
||||
|
||||
assert checks == ('data', 'archives')
|
||||
|
||||
|
||||
def test_parse_checks_with_data_check_passes_through_archives():
|
||||
checks = module._parse_checks({'checks': ['data', 'archives']})
|
||||
|
||||
assert checks == ('data', 'archives')
|
||||
|
||||
|
||||
def test_parse_checks_prefers_override_checks_to_configured_checks():
|
||||
checks = module._parse_checks({'checks': ['archives']}, only_checks=['repository', 'extract'])
|
||||
|
||||
assert checks == ('repository', 'extract')
|
||||
|
||||
|
||||
def test_parse_checks_with_override_data_check_also_injects_archives():
|
||||
checks = module._parse_checks({'checks': ['extract']}, only_checks=['data'])
|
||||
|
||||
assert checks == ('data', 'archives')
|
||||
|
||||
|
||||
def test_make_check_flags_with_repository_check_returns_flag():
|
||||
flags = module._make_check_flags(('repository',))
|
||||
|
||||
assert flags == ('--repository-only',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_archives_check_returns_flag():
|
||||
flags = module._make_check_flags(('archives',))
|
||||
|
||||
assert flags == ('--archives-only',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_data_check_returns_flag():
|
||||
flags = module._make_check_flags(('data',))
|
||||
|
||||
assert flags == ('--verify-data',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_extract_omits_extract_flag():
|
||||
flags = module._make_check_flags(('extract',))
|
||||
|
||||
assert flags == ()
|
||||
|
||||
|
||||
def test_make_check_flags_with_default_checks_returns_default_flags():
|
||||
flags = module._make_check_flags(module.DEFAULT_CHECKS)
|
||||
def test_make_check_flags_with_default_checks_and_default_prefix_returns_default_flags():
|
||||
flags = module._make_check_flags(module.DEFAULT_CHECKS, prefix=module.DEFAULT_PREFIX)
|
||||
|
||||
assert flags == ('--prefix', module.DEFAULT_PREFIX)
|
||||
|
||||
|
||||
def test_make_check_flags_with_all_checks_returns_default_flags():
|
||||
flags = module._make_check_flags(module.DEFAULT_CHECKS + ('extract',))
|
||||
def test_make_check_flags_with_all_checks_and_default_prefix_returns_default_flags():
|
||||
flags = module._make_check_flags(
|
||||
module.DEFAULT_CHECKS + ('extract',), prefix=module.DEFAULT_PREFIX
|
||||
)
|
||||
|
||||
assert flags == ('--prefix', module.DEFAULT_PREFIX)
|
||||
|
||||
|
|
@ -67,7 +111,7 @@ def test_make_check_flags_with_all_checks_returns_default_flags():
|
|||
def test_make_check_flags_with_archives_check_and_last_includes_last_flag():
|
||||
flags = module._make_check_flags(('archives',), check_last=3)
|
||||
|
||||
assert flags == ('--archives-only', '--last', '3', '--prefix', module.DEFAULT_PREFIX)
|
||||
assert flags == ('--archives-only', '--last', '3')
|
||||
|
||||
|
||||
def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
|
||||
|
|
@ -79,7 +123,7 @@ def test_make_check_flags_with_repository_check_and_last_omits_last_flag():
|
|||
def test_make_check_flags_with_default_checks_and_last_includes_last_flag():
|
||||
flags = module._make_check_flags(module.DEFAULT_CHECKS, check_last=3)
|
||||
|
||||
assert flags == ('--last', '3', '--prefix', module.DEFAULT_PREFIX)
|
||||
assert flags == ('--last', '3')
|
||||
|
||||
|
||||
def test_make_check_flags_with_archives_check_and_prefix_includes_prefix_flag():
|
||||
|
|
@ -88,6 +132,18 @@ def test_make_check_flags_with_archives_check_and_prefix_includes_prefix_flag():
|
|||
assert flags == ('--archives-only', '--prefix', 'foo-')
|
||||
|
||||
|
||||
def test_make_check_flags_with_archives_check_and_empty_prefix_omits_prefix_flag():
|
||||
flags = module._make_check_flags(('archives',), prefix='')
|
||||
|
||||
assert flags == ('--archives-only',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_archives_check_and_none_prefix_omits_prefix_flag():
|
||||
flags = module._make_check_flags(('archives',), prefix=None)
|
||||
|
||||
assert flags == ('--archives-only',)
|
||||
|
||||
|
||||
def test_make_check_flags_with_repository_check_and_prefix_omits_prefix_flag():
|
||||
flags = module._make_check_flags(('repository',), prefix='foo-')
|
||||
|
||||
|
|
@ -114,7 +170,7 @@ def test_check_archives_calls_borg_with_parameters(checks):
|
|||
consistency_config = {'check_last': check_last}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').with_args(
|
||||
checks, check_last, None
|
||||
checks, check_last, module.DEFAULT_PREFIX
|
||||
).and_return(())
|
||||
insert_execute_command_mock(('borg', 'check', 'repo'))
|
||||
|
||||
|
|
@ -143,7 +199,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_execute_command_mock(('borg', 'check', 'repo', '--info'))
|
||||
insert_execute_command_mock(('borg', 'check', '--info', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={}, consistency_config=consistency_config
|
||||
|
|
@ -156,7 +212,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_execute_command_mock(('borg', 'check', 'repo', '--debug', '--show-rc'))
|
||||
insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={}, consistency_config=consistency_config
|
||||
|
|
@ -179,7 +235,7 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
|
|||
consistency_config = {'check_last': check_last}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').with_args(
|
||||
checks, check_last, None
|
||||
checks, check_last, module.DEFAULT_PREFIX
|
||||
).and_return(())
|
||||
insert_execute_command_mock(('borg1', 'check', 'repo'))
|
||||
|
||||
|
|
@ -197,9 +253,9 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||
consistency_config = {'check_last': check_last}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').with_args(
|
||||
checks, check_last, None
|
||||
checks, check_last, module.DEFAULT_PREFIX
|
||||
).and_return(())
|
||||
insert_execute_command_mock(('borg', 'check', 'repo', '--remote-path', 'borg1'))
|
||||
insert_execute_command_mock(('borg', 'check', '--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository='repo',
|
||||
|
|
@ -215,9 +271,9 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
|||
consistency_config = {'check_last': check_last}
|
||||
flexmock(module).should_receive('_parse_checks').and_return(checks)
|
||||
flexmock(module).should_receive('_make_check_flags').with_args(
|
||||
checks, check_last, None
|
||||
checks, check_last, module.DEFAULT_PREFIX
|
||||
).and_return(())
|
||||
insert_execute_command_mock(('borg', 'check', 'repo', '--lock-wait', '5'))
|
||||
insert_execute_command_mock(('borg', 'check', '--lock-wait', '5', 'repo'))
|
||||
|
||||
module.check_archives(
|
||||
repository='repo', storage_config={'lock_wait': 5}, consistency_config=consistency_config
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import create as module
|
||||
|
|
@ -155,18 +156,33 @@ def test_make_exclude_flags_is_empty_when_config_has_no_excludes():
|
|||
assert exclude_flags == ()
|
||||
|
||||
|
||||
def test_borgmatic_source_directories_set_when_directory_exists():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.os.path).should_receive('expanduser')
|
||||
|
||||
assert module.borgmatic_source_directories() == [module.BORGMATIC_SOURCE_DIRECTORY]
|
||||
|
||||
|
||||
def test_borgmatic_source_directories_empty_when_directory_does_not_exist():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os.path).should_receive('expanduser')
|
||||
|
||||
assert module.borgmatic_source_directories() == []
|
||||
|
||||
|
||||
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
|
||||
CREATE_COMMAND = ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar')
|
||||
ARCHIVE_WITH_PATHS = ('repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'bar')
|
||||
|
||||
|
||||
def test_create_archive_calls_borg_with_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND, output_log_level=logging.INFO
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -183,6 +199,7 @@ def test_create_archive_calls_borg_with_parameters():
|
|||
|
||||
def test_create_archive_with_patterns_calls_borg_with_patterns():
|
||||
pattern_flags = ('--patterns-from', 'patterns')
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(
|
||||
|
|
@ -191,7 +208,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, output_log_level=logging.INFO
|
||||
('borg', 'create') + pattern_flags + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -208,6 +225,7 @@ def test_create_archive_with_patterns_calls_borg_with_patterns():
|
|||
|
||||
def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||
exclude_flags = ('--exclude-from', 'excludes')
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(('exclude',))
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None).and_return(
|
||||
|
|
@ -216,7 +234,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, output_log_level=logging.INFO
|
||||
('borg', 'create') + exclude_flags + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -232,6 +250,7 @@ def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
|||
|
||||
|
||||
def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -239,7 +258,7 @@ 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'),
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--stats') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
|
@ -257,6 +276,7 @@ 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('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -264,7 +284,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
|||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--json',), output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
|
|
@ -282,13 +302,15 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
|||
|
||||
|
||||
def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc'),
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
|
@ -306,13 +328,14 @@ 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('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--json',), output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
|
|
@ -330,6 +353,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
|||
|
||||
|
||||
def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -337,7 +361,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',), output_log_level=logging.INFO
|
||||
('borg', 'create', '--dry-run') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -355,6 +379,7 @@ def test_create_archive_with_dry_run_calls_borg_with_dry_run_parameter():
|
|||
def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_parameter():
|
||||
# --dry-run and --stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -362,7 +387,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'),
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--info', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
|
@ -382,6 +408,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
|||
def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_parameter():
|
||||
# --dry-run and --stats are mutually exclusive, see:
|
||||
# https://borgbackup.readthedocs.io/en/stable/usage/create.html#description
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -389,7 +416,8 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
|
|||
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-', '--debug', '--show-rc', '--dry-run'),
|
||||
('borg', 'create', '--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run')
|
||||
+ ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
|
@ -407,13 +435,15 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
|
|||
|
||||
|
||||
def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_interval_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--checkpoint-interval', '600'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--checkpoint-interval', '600') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -429,13 +459,15 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
|||
|
||||
|
||||
def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--chunker-params', '1,2,3,4'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--chunker-params', '1,2,3,4') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -451,13 +483,15 @@ def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_param
|
|||
|
||||
|
||||
def test_create_archive_with_compression_calls_borg_with_compression_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--compression', 'rle'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--compression', 'rle') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -473,13 +507,15 @@ def test_create_archive_with_compression_calls_borg_with_compression_parameters(
|
|||
|
||||
|
||||
def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--remote-ratelimit', '100'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--remote-ratelimit', '100') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -495,13 +531,14 @@ def test_create_archive_with_remote_rate_limit_calls_borg_with_remote_ratelimit_
|
|||
|
||||
|
||||
def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--one-file-system',), output_log_level=logging.INFO
|
||||
('borg', 'create', '--one-file-system') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -518,13 +555,14 @@ def test_create_archive_with_one_file_system_calls_borg_with_one_file_system_par
|
|||
|
||||
|
||||
def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--numeric-owner',), output_log_level=logging.INFO
|
||||
('borg', 'create', '--numeric-owner') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -541,13 +579,14 @@ def test_create_archive_with_numeric_owner_calls_borg_with_numeric_owner_paramet
|
|||
|
||||
|
||||
def test_create_archive_with_read_special_calls_borg_with_read_special_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--read-special',), output_log_level=logging.INFO
|
||||
('borg', 'create', '--read-special') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -563,14 +602,16 @@ def test_create_archive_with_read_special_calls_borg_with_read_special_parameter
|
|||
)
|
||||
|
||||
|
||||
def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parameter():
|
||||
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||
def test_create_archive_with_option_true_calls_borg_without_corresponding_parameter(option_name):
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND, output_log_level=logging.INFO
|
||||
('borg', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -579,21 +620,24 @@ def test_create_archive_with_bsd_flags_true_calls_borg_without_nobsdflags_parame
|
|||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'bsd_flags': True,
|
||||
option_name: True,
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_parameter():
|
||||
@pytest.mark.parametrize('option_name', ('atime', 'ctime', 'birthtime', 'bsd_flags'))
|
||||
def test_create_archive_with_option_false_calls_borg_with_corresponding_parameter(option_name):
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--nobsdflags',), output_log_level=logging.INFO
|
||||
('borg', 'create', '--no' + option_name.replace('_', '')) + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -602,7 +646,7 @@ def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_paramete
|
|||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'bsd_flags': False,
|
||||
option_name: False,
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
|
|
@ -610,13 +654,15 @@ def test_create_archive_with_bsd_flags_false_calls_borg_with_nobsdflags_paramete
|
|||
|
||||
|
||||
def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--files-cache', 'ctime,size'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--files-cache', 'ctime,size') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -633,13 +679,14 @@ def test_create_archive_with_files_cache_calls_borg_with_files_cache_parameters(
|
|||
|
||||
|
||||
def test_create_archive_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1',) + CREATE_COMMAND[1:], output_log_level=logging.INFO
|
||||
('borg1', 'create') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -656,13 +703,15 @@ def test_create_archive_with_local_path_calls_borg_via_local_path():
|
|||
|
||||
|
||||
def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--remote-path', 'borg1'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--remote-path', 'borg1') + ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -679,13 +728,14 @@ def test_create_archive_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||
|
||||
|
||||
def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--umask', '740'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--umask', '740') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -701,13 +751,14 @@ def test_create_archive_with_umask_calls_borg_with_umask_parameters():
|
|||
|
||||
|
||||
def test_create_archive_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--lock-wait', '5'), output_log_level=logging.INFO
|
||||
('borg', 'create', '--lock-wait', '5') + ARCHIVE_WITH_PATHS, output_log_level=logging.INFO
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -723,13 +774,14 @@ 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('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--stats',), output_log_level=logging.WARNING
|
||||
('borg', 'create', '--stats') + ARCHIVE_WITH_PATHS, output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
|
|
@ -745,14 +797,39 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter():
|
|||
)
|
||||
|
||||
|
||||
def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'create', '--progress') + ARCHIVE_WITH_PATHS
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={},
|
||||
progress=True,
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--json',), output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
|
|
@ -771,13 +848,14 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
|||
|
||||
|
||||
def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
CREATE_COMMAND + ('--json',), output_log_level=None
|
||||
('borg', 'create', '--json') + ARCHIVE_WITH_PATHS, output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.create_archive(
|
||||
|
|
@ -797,6 +875,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
|||
|
||||
|
||||
def test_create_archive_with_source_directories_glob_expands():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -821,6 +900,7 @@ def test_create_archive_with_source_directories_glob_expands():
|
|||
|
||||
|
||||
def test_create_archive_with_non_matching_source_directories_glob_passes_through():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo*',))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -845,6 +925,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
|
|||
|
||||
|
||||
def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'food'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -868,6 +949,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
|
|||
|
||||
|
||||
def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
@ -890,6 +972,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
|
|||
|
||||
|
||||
def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
|
||||
flexmock(module).should_receive('borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('_expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
|
|
|||
|
|
@ -36,13 +36,27 @@ def test_initialize_with_ssh_command_should_set_environment():
|
|||
os.environ = orig_environ
|
||||
|
||||
|
||||
def test_initialize_without_configuration_should_not_set_environment():
|
||||
def test_initialize_without_configuration_should_only_set_default_environment():
|
||||
orig_environ = os.environ
|
||||
|
||||
try:
|
||||
os.environ = {}
|
||||
module.initialize({})
|
||||
|
||||
assert sum(1 for key in os.environ.keys() if key.startswith('BORG_')) == 0
|
||||
assert {key: value for key, value in os.environ.items() if key.startswith('BORG_')} == {
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
|
||||
}
|
||||
finally:
|
||||
os.environ = orig_environ
|
||||
|
||||
|
||||
def test_initialize_with_relocated_repo_access_should_override_default():
|
||||
orig_environ = os.environ
|
||||
|
||||
try:
|
||||
os.environ = {}
|
||||
module.initialize({'relocated_repo_access_is_ok': True})
|
||||
assert os.environ.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
|
||||
finally:
|
||||
os.environ = orig_environ
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ def test_extract_last_archive_dry_run_without_any_archives_should_not_raise():
|
|||
|
||||
def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_parameter():
|
||||
insert_execute_command_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--info'), result='archive1\narchive2\n'
|
||||
('borg', 'list', '--short', '--info', 'repo'), result='archive1\narchive2\n'
|
||||
)
|
||||
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive2', '--info'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--dry-run', '--info', 'repo::archive2'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.extract_last_archive_dry_run(repository='repo', lock_wait=None)
|
||||
|
|
@ -44,10 +44,10 @@ def test_extract_last_archive_dry_run_with_log_info_calls_borg_with_info_paramet
|
|||
|
||||
def test_extract_last_archive_dry_run_with_log_debug_calls_borg_with_debug_parameter():
|
||||
insert_execute_command_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--debug', '--show-rc'), result='archive1\narchive2\n'
|
||||
('borg', 'list', '--short', '--debug', '--show-rc', 'repo'), result='archive1\narchive2\n'
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--debug', '--show-rc', '--list')
|
||||
('borg', 'extract', '--dry-run', '--debug', '--show-rc', '--list', 'repo::archive2')
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
|
|
@ -65,10 +65,10 @@ def test_extract_last_archive_dry_run_calls_borg_via_local_path():
|
|||
|
||||
def test_extract_last_archive_dry_run_calls_borg_with_remote_path_parameters():
|
||||
insert_execute_command_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--remote-path', 'borg1'), result='archive1\narchive2\n'
|
||||
('borg', 'list', '--short', '--remote-path', 'borg1', 'repo'), result='archive1\narchive2\n'
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--remote-path', 'borg1')
|
||||
('borg', 'extract', '--dry-run', '--remote-path', 'borg1', 'repo::archive2')
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(repository='repo', lock_wait=None, remote_path='borg1')
|
||||
|
|
@ -76,10 +76,10 @@ 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():
|
||||
insert_execute_command_output_mock(
|
||||
('borg', 'list', '--short', 'repo', '--lock-wait', '5'), result='archive1\narchive2\n'
|
||||
('borg', 'list', '--short', '--lock-wait', '5', 'repo'), result='archive1\narchive2\n'
|
||||
)
|
||||
insert_execute_command_mock(
|
||||
('borg', 'extract', '--dry-run', 'repo::archive2', '--lock-wait', '5')
|
||||
('borg', 'extract', '--dry-run', '--lock-wait', '5', 'repo::archive2')
|
||||
)
|
||||
|
||||
module.extract_last_archive_dry_run(repository='repo', lock_wait=5)
|
||||
|
|
@ -99,7 +99,7 @@ def test_extract_archive_calls_borg_with_restore_path_parameters():
|
|||
|
||||
|
||||
def test_extract_archive_calls_borg_with_remote_path_parameters():
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--remote-path', 'borg1'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--remote-path', 'borg1', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
@ -113,7 +113,7 @@ def test_extract_archive_calls_borg_with_remote_path_parameters():
|
|||
|
||||
|
||||
def test_extract_archive_calls_borg_with_numeric_owner_parameter():
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--numeric-owner'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--numeric-owner', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
@ -126,7 +126,7 @@ def test_extract_archive_calls_borg_with_numeric_owner_parameter():
|
|||
|
||||
|
||||
def test_extract_archive_calls_borg_with_umask_parameters():
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--umask', '0770'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--umask', '0770', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
@ -139,7 +139,7 @@ def test_extract_archive_calls_borg_with_umask_parameters():
|
|||
|
||||
|
||||
def test_extract_archive_calls_borg_with_lock_wait_parameters():
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--lock-wait', '5'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--lock-wait', '5', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
@ -152,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_execute_command_mock(('borg', 'extract', 'repo::archive', '--info'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--info', 'repo::archive'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.extract_archive(
|
||||
|
|
@ -167,7 +167,7 @@ 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_execute_command_mock(
|
||||
('borg', 'extract', 'repo::archive', '--debug', '--list', '--show-rc')
|
||||
('borg', 'extract', '--debug', '--list', '--show-rc', 'repo::archive')
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
|
|
@ -182,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_execute_command_mock(('borg', 'extract', 'repo::archive', '--dry-run'))
|
||||
insert_execute_command_mock(('borg', 'extract', '--dry-run', 'repo::archive'))
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=True,
|
||||
|
|
@ -195,7 +195,9 @@ def test_extract_archive_calls_borg_with_dry_run_parameter():
|
|||
|
||||
|
||||
def test_extract_archive_calls_borg_with_progress_parameter():
|
||||
insert_execute_command_mock(('borg', 'extract', 'repo::archive', '--progress'))
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
('borg', 'extract', '--progress', 'repo::archive')
|
||||
).once()
|
||||
|
||||
module.extract_archive(
|
||||
dry_run=False,
|
||||
|
|
|
|||
47
tests/unit/borg/test_flags.py
Normal file
47
tests/unit/borg/test_flags.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import flags as module
|
||||
|
||||
|
||||
def test_make_flags_formats_string_value():
|
||||
assert module.make_flags('foo', 'bar') == ('--foo', 'bar')
|
||||
|
||||
|
||||
def test_make_flags_formats_integer_value():
|
||||
assert module.make_flags('foo', 3) == ('--foo', '3')
|
||||
|
||||
|
||||
def test_make_flags_formats_true_value():
|
||||
assert module.make_flags('foo', True) == ('--foo',)
|
||||
|
||||
|
||||
def test_make_flags_omits_false_value():
|
||||
assert module.make_flags('foo', False) == ()
|
||||
|
||||
|
||||
def test_make_flags_formats_name_with_underscore():
|
||||
assert module.make_flags('posix_me_harder', 'okay') == ('--posix-me-harder', 'okay')
|
||||
|
||||
|
||||
def test_make_flags_from_arguments_flattens_and_sorts_multiple_arguments():
|
||||
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('make_flags').with_args('baz', 'quux').and_return(
|
||||
('baz', 'quux')
|
||||
)
|
||||
arguments = flexmock(foo='bar', baz='quux')
|
||||
|
||||
assert module.make_flags_from_arguments(arguments) == ('baz', 'quux', 'foo', 'bar')
|
||||
|
||||
|
||||
def test_make_flags_from_arguments_excludes_underscored_argument_names():
|
||||
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||
arguments = flexmock(foo='bar', _baz='quux')
|
||||
|
||||
assert module.make_flags_from_arguments(arguments) == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_make_flags_from_arguments_omits_excludes():
|
||||
flexmock(module).should_receive('make_flags').with_args('foo', 'bar').and_return(('foo', 'bar'))
|
||||
arguments = flexmock(foo='bar', baz='quux')
|
||||
|
||||
assert module.make_flags_from_arguments(arguments, excludes=('baz', 'other')) == ('foo', 'bar')
|
||||
|
|
@ -1,91 +1,140 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import info as module
|
||||
|
||||
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, output_log_level=logging.WARNING
|
||||
('borg', 'info', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.display_archives_info(repository='repo', storage_config={})
|
||||
module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
INFO_COMMAND + ('--info',), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--info', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
module.display_archives_info(repository='repo', storage_config={})
|
||||
module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.INFO)
|
||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
||||
json_output = module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, 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'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--debug', '--show-rc', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.display_archives_info(repository='repo', storage_config={})
|
||||
module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=False)
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
||||
json_output = module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, 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',), output_log_level=None
|
||||
('borg', 'info', '--json', 'repo'), output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.display_archives_info(repository='repo', storage_config={}, json=True)
|
||||
json_output = module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive=None, json=True)
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
|
||||
|
||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||
def test_display_archives_info_with_archive_calls_borg_with_archive_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1',) + INFO_COMMAND[1:], output_log_level=logging.WARNING
|
||||
('borg', 'info', 'repo::archive'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.display_archives_info(repository='repo', storage_config={}, local_path='borg1')
|
||||
module.display_archives_info(
|
||||
repository='repo', storage_config={}, info_arguments=flexmock(archive='archive', json=False)
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'info', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
info_arguments=flexmock(archive=None, json=False),
|
||||
local_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
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'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--remote-path', 'borg1', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.display_archives_info(repository='repo', storage_config={}, remote_path='borg1')
|
||||
module.display_archives_info(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
info_arguments=flexmock(archive=None, json=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
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'), output_log_level=logging.WARNING
|
||||
('borg', 'info', '--lock-wait', '5', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.display_archives_info(repository='repo', storage_config=storage_config)
|
||||
module.display_archives_info(
|
||||
repository='repo',
|
||||
storage_config=storage_config,
|
||||
info_arguments=flexmock(archive=None, json=False),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('argument_name', ('prefix', 'glob_archives', 'sort_by', 'first', 'last'))
|
||||
def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
info_arguments=flexmock(archive=None, json=False, **{argument_name: 'value'}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from borgmatic.borg import init as module
|
|||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
INFO_SOME_UNKNOWN_EXIT_CODE = -999
|
||||
INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey')
|
||||
INIT_COMMAND = ('borg', 'init', '--encryption', 'repokey')
|
||||
|
||||
|
||||
def insert_info_command_found_mock():
|
||||
|
|
@ -23,21 +23,31 @@ def insert_info_command_not_found_mock():
|
|||
|
||||
|
||||
def insert_init_command_mock(init_command, **kwargs):
|
||||
flexmock(module.subprocess).should_receive('check_call').with_args(
|
||||
init_command, **kwargs
|
||||
flexmock(module).should_receive('execute_command_without_capture').with_args(
|
||||
init_command
|
||||
).once()
|
||||
|
||||
|
||||
def test_initialize_repository_calls_borg_with_parameters():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND)
|
||||
insert_init_command_mock(INIT_COMMAND + ('repo',))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_raises_for_borg_init_error():
|
||||
insert_info_command_not_found_mock()
|
||||
flexmock(module).should_receive('execute_command_without_capture').and_raise(
|
||||
module.subprocess.CalledProcessError(2, 'borg init')
|
||||
)
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
||||
insert_info_command_found_mock()
|
||||
flexmock(module.subprocess).should_receive('check_call').never()
|
||||
flexmock(module).should_receive('execute_command_without_capture').never()
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
|
@ -53,21 +63,21 @@ def test_initialize_repository_raises_for_unknown_info_command_error():
|
|||
|
||||
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
|
||||
insert_info_command_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--append-only',))
|
||||
insert_init_command_mock(INIT_COMMAND + ('--append-only', 'repo'))
|
||||
|
||||
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_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G'))
|
||||
insert_init_command_mock(INIT_COMMAND + ('--storage-quota', '5G', 'repo'))
|
||||
|
||||
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_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--info',))
|
||||
insert_init_command_mock(INIT_COMMAND + ('--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
|
@ -75,7 +85,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_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--debug',))
|
||||
insert_init_command_mock(INIT_COMMAND + ('--debug', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
|
@ -83,13 +93,13 @@ 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_not_found_mock()
|
||||
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:])
|
||||
insert_init_command_mock(('borg1',) + INIT_COMMAND[1:] + ('repo',))
|
||||
|
||||
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_not_found_mock()
|
||||
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
|
||||
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')
|
||||
|
|
|
|||
|
|
@ -1,65 +1,88 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import list as module
|
||||
|
||||
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, output_log_level=logging.WARNING
|
||||
('borg', 'list', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={})
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
LIST_COMMAND + ('--info',), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--info', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={})
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={}, json=True)
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
|
||||
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'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--debug', '--show-rc', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={})
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={}, json=True)
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
|
||||
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'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--lock-wait', '5', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.list_archives(repository='repo', storage_config=storage_config)
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config=storage_config,
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
||||
|
|
@ -68,30 +91,102 @@ def test_list_archives_with_archive_calls_borg_with_archive_parameter():
|
|||
('borg', 'list', 'repo::archive'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.list_archives(repository='repo', storage_config=storage_config, archive='archive')
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config=storage_config,
|
||||
list_arguments=flexmock(archive='archive', json=False, successful=False),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1',) + LIST_COMMAND[1:], output_log_level=logging.WARNING
|
||||
('borg1', 'list', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={}, local_path='borg1')
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
local_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
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'), output_log_level=logging.WARNING
|
||||
('borg', 'list', '--remote-path', 'borg1', 'repo'), output_log_level=logging.WARNING
|
||||
)
|
||||
|
||||
module.list_archives(repository='repo', storage_config={}, remote_path='borg1')
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_short_calls_borg_with_short_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--short', 'repo'), output_log_level=logging.WARNING
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=False, short=True),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'argument_name',
|
||||
(
|
||||
'prefix',
|
||||
'glob_archives',
|
||||
'sort_by',
|
||||
'first',
|
||||
'last',
|
||||
'exclude',
|
||||
'exclude_from',
|
||||
'pattern',
|
||||
'patterns_from',
|
||||
),
|
||||
)
|
||||
def test_list_archives_passes_through_arguments_to_borg(argument_name):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--' + argument_name.replace('_', '-'), 'value', 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(
|
||||
archive=None, json=False, successful=False, **{argument_name: 'value'}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_successful_calls_borg_to_exclude_checkpoints():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'list', '--glob-archives', module.BORG_EXCLUDE_CHECKPOINTS_GLOB, 'repo'),
|
||||
output_log_level=logging.WARNING,
|
||||
).and_return('[]')
|
||||
|
||||
module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=False, successful=True),
|
||||
)
|
||||
|
||||
|
||||
def test_list_archives_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
LIST_COMMAND + ('--json',), output_log_level=None
|
||||
('borg', 'list', '--json', 'repo'), output_log_level=None
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.list_archives(repository='repo', storage_config={}, json=True)
|
||||
json_output = module.list_archives(
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
list_arguments=flexmock(archive=None, json=True, successful=False),
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ from borgmatic.borg import prune as module
|
|||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(prune_command, **kwargs):
|
||||
flexmock(module).should_receive('execute_command').with_args(prune_command).once()
|
||||
def insert_execute_command_mock(prune_command, output_log_level):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
prune_command, output_log_level=output_log_level
|
||||
).once()
|
||||
|
||||
|
||||
BASE_PRUNE_FLAGS = (('--keep-daily', '1'), ('--keep-weekly', '2'), ('--keep-monthly', '3'))
|
||||
|
|
@ -33,17 +35,27 @@ def test_make_prune_flags_accepts_prefix_with_placeholders():
|
|||
assert tuple(result) == expected
|
||||
|
||||
|
||||
PRUNE_COMMAND = (
|
||||
'borg',
|
||||
'prune',
|
||||
'repo',
|
||||
'--keep-daily',
|
||||
'1',
|
||||
'--keep-weekly',
|
||||
'2',
|
||||
'--keep-monthly',
|
||||
'3',
|
||||
)
|
||||
def test_make_prune_flags_treats_empty_prefix_as_no_prefix():
|
||||
retention_config = OrderedDict((('keep_daily', 1), ('prefix', '')))
|
||||
|
||||
result = module._make_prune_flags(retention_config)
|
||||
|
||||
expected = (('--keep-daily', '1'),)
|
||||
|
||||
assert tuple(result) == expected
|
||||
|
||||
|
||||
def test_make_prune_flags_treats_none_prefix_as_no_prefix():
|
||||
retention_config = OrderedDict((('keep_daily', 1), ('prefix', None)))
|
||||
|
||||
result = module._make_prune_flags(retention_config)
|
||||
|
||||
expected = (('--keep-daily', '1'),)
|
||||
|
||||
assert tuple(result) == expected
|
||||
|
||||
|
||||
PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3')
|
||||
|
||||
|
||||
def test_prune_archives_calls_borg_with_parameters():
|
||||
|
|
@ -51,7 +63,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_execute_command_mock(PRUNE_COMMAND)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('repo',), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False, repository='repo', storage_config={}, retention_config=retention_config
|
||||
|
|
@ -63,7 +75,7 @@ def test_prune_archives_with_log_info_calls_borg_with_info_parameter():
|
|||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', '--info', 'repo'), logging.INFO)
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
|
|
@ -76,7 +88,9 @@ 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_execute_command_mock(PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc'))
|
||||
insert_execute_command_mock(
|
||||
PRUNE_COMMAND + ('--stats', '--debug', '--list', '--show-rc', 'repo'), logging.INFO
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.prune_archives(
|
||||
|
|
@ -89,7 +103,7 @@ def test_prune_archives_with_dry_run_calls_borg_with_dry_run_parameter():
|
|||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run',))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--dry-run', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
repository='repo', storage_config={}, dry_run=True, retention_config=retention_config
|
||||
|
|
@ -101,7 +115,7 @@ def test_prune_archives_with_local_path_calls_borg_via_local_path():
|
|||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:])
|
||||
insert_execute_command_mock(('borg1',) + PRUNE_COMMAND[1:] + ('repo',), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
|
|
@ -117,7 +131,7 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--remote-path', 'borg1', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
|
|
@ -128,13 +142,29 @@ def test_prune_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
|||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_stats_calls_borg_with_stats_parameter():
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--stats', 'repo'), logging.WARNING)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
storage_config={},
|
||||
retention_config=retention_config,
|
||||
stats=True,
|
||||
)
|
||||
|
||||
|
||||
def test_prune_archives_with_umask_calls_borg_with_umask_parameters():
|
||||
storage_config = {'umask': '077'}
|
||||
retention_config = flexmock()
|
||||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--umask', '077', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
|
|
@ -150,7 +180,7 @@ def test_prune_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
|||
flexmock(module).should_receive('_make_prune_flags').with_args(retention_config).and_return(
|
||||
BASE_PRUNE_FLAGS
|
||||
)
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5'))
|
||||
insert_execute_command_mock(PRUNE_COMMAND + ('--lock-wait', '5', 'repo'), logging.INFO)
|
||||
|
||||
module.prune_archives(
|
||||
dry_run=False,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ from borgmatic.commands import arguments as module
|
|||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
|
||||
global_namespace = flexmock()
|
||||
action_namespace = flexmock(foo=True)
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
||||
|
|
@ -14,17 +12,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser
|
|||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('--foo', 'true', 'action'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
assert arguments == {'action': action_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
|
||||
global_namespace = flexmock()
|
||||
action_namespace = flexmock(foo=True)
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
|
||||
|
|
@ -32,57 +26,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_
|
|||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('action', '--foo', 'true'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), subparsers)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_global_arguments_before_subparser_name():
|
||||
global_namespace = flexmock(verbosity='lots')
|
||||
action_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(
|
||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||
),
|
||||
'other': flexmock(),
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_global_arguments_after_subparser_name():
|
||||
global_namespace = flexmock(verbosity='lots')
|
||||
action_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(
|
||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||
),
|
||||
'other': flexmock(),
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('action', '--verbosity', 'lots'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
assert arguments == {'action': action_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
||||
global_namespace = flexmock()
|
||||
action_namespace = flexmock(foo=True)
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
|
|
@ -94,18 +44,14 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
|
|||
)
|
||||
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('-a', '--foo', 'true'), top_level_parser, subparsers
|
||||
)
|
||||
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
|
||||
|
||||
assert arguments == {'action': action_namespace, 'global': global_namespace}
|
||||
assert arguments == {'action': action_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
||||
global_namespace = flexmock()
|
||||
action_namespace = flexmock(foo=True)
|
||||
other_namespace = flexmock(bar=3)
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(
|
||||
|
|
@ -116,22 +62,16 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
|
|||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
('action', '--foo', 'true', 'other', '--bar', '3'), top_level_parser, subparsers
|
||||
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
|
||||
)
|
||||
|
||||
assert arguments == {
|
||||
'action': action_namespace,
|
||||
'other': other_namespace,
|
||||
'global': global_namespace,
|
||||
}
|
||||
assert arguments == {'action': action_namespace, 'other': other_namespace}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_applies_default_subparsers():
|
||||
global_namespace = flexmock()
|
||||
prune_namespace = flexmock()
|
||||
create_namespace = flexmock(progress=True)
|
||||
check_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
|
||||
|
|
@ -141,17 +81,16 @@ def test_parse_subparser_arguments_applies_default_subparsers():
|
|||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(('--progress'), top_level_parser, subparsers)
|
||||
arguments = module.parse_subparser_arguments(('--progress'), subparsers)
|
||||
|
||||
assert arguments == {
|
||||
'prune': prune_namespace,
|
||||
'create': create_namespace,
|
||||
'check': check_namespace,
|
||||
'global': global_namespace,
|
||||
}
|
||||
|
||||
|
||||
def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers():
|
||||
def test_parse_global_arguments_with_help_does_not_apply_default_subparsers():
|
||||
global_namespace = flexmock(verbosity='lots')
|
||||
action_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
|
|
@ -164,8 +103,48 @@ def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers()
|
|||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_subparser_arguments(
|
||||
arguments = module.parse_global_arguments(
|
||||
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == {'global': global_namespace}
|
||||
assert arguments == global_namespace
|
||||
|
||||
|
||||
def test_parse_global_arguments_consumes_global_arguments_before_subparser_name():
|
||||
global_namespace = flexmock(verbosity='lots')
|
||||
action_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(
|
||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||
),
|
||||
'other': flexmock(),
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_global_arguments(
|
||||
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == global_namespace
|
||||
|
||||
|
||||
def test_parse_global_arguments_consumes_global_arguments_after_subparser_name():
|
||||
global_namespace = flexmock(verbosity='lots')
|
||||
action_namespace = flexmock()
|
||||
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
|
||||
subparsers = flexmock(
|
||||
choices={
|
||||
'action': flexmock(
|
||||
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
|
||||
),
|
||||
'other': flexmock(),
|
||||
}
|
||||
)
|
||||
|
||||
arguments = module.parse_global_arguments(
|
||||
('action', '--verbosity', 'lots'), top_level_parser, subparsers
|
||||
)
|
||||
|
||||
assert arguments == global_namespace
|
||||
|
|
|
|||
|
|
@ -1,8 +1,100 @@
|
|||
import logging
|
||||
import subprocess
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.commands import borgmatic as module
|
||||
|
||||
|
||||
def test_run_configuration_runs_actions_for_each_repository():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('run_actions').and_return(expected_results[:1]).and_return(
|
||||
expected_results[1:]
|
||||
)
|
||||
config = {'location': {'repositories': ['foo', 'bar']}}
|
||||
arguments = {'global': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_executes_hooks_for_create_action():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').twice()
|
||||
flexmock(module.postgresql).should_receive('dump_databases').once()
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks').twice()
|
||||
flexmock(module.postgresql).should_receive('remove_database_dumps').once()
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
|
||||
def test_run_configuration_logs_actions_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(module.postgresql).should_receive('dump_databases')
|
||||
flexmock(module.healthchecks).should_receive('ping_healthchecks')
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False)}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_logs_pre_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(OSError).and_return(None)
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').never()
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_logs_post_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(
|
||||
OSError
|
||||
).and_return(None)
|
||||
expected_results = [flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_results)
|
||||
flexmock(module).should_receive('run_actions').and_return([])
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False), 'create': flexmock()}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_logs_on_error_hook_error():
|
||||
flexmock(module.borg_environment).should_receive('initialize')
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(OSError)
|
||||
expected_results = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(
|
||||
expected_results[:1]
|
||||
).and_return(expected_results[1:])
|
||||
flexmock(module).should_receive('run_actions').and_raise(OSError)
|
||||
config = {'location': {'repositories': ['foo']}}
|
||||
arguments = {'global': flexmock(dry_run=False)}
|
||||
|
||||
results = list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_load_configurations_collects_parsed_configurations():
|
||||
configuration = flexmock()
|
||||
other_configuration = flexmock()
|
||||
|
|
@ -22,10 +114,46 @@ def test_load_configurations_logs_critical_for_parse_error():
|
|||
configs, logs = tuple(module.load_configurations(('test.yaml',)))
|
||||
|
||||
assert configs == {}
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_output_logs_for_message_only():
|
||||
logs = tuple(module.make_error_log_records('Error'))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_output_logs_for_called_process_error():
|
||||
logs = tuple(
|
||||
module.make_error_log_records(
|
||||
'Error', subprocess.CalledProcessError(1, 'ls', 'error output')
|
||||
)
|
||||
)
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
assert any(log for log in logs if 'error output' in str(log))
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_logs_for_value_error():
|
||||
logs = tuple(module.make_error_log_records('Error', ValueError()))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_logs_for_os_error():
|
||||
logs = tuple(module.make_error_log_records('Error', OSError()))
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_make_error_log_records_generates_nothing_for_other_error():
|
||||
logs = tuple(module.make_error_log_records('Error', KeyError()))
|
||||
|
||||
assert logs == ()
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
flexmock(module.command).should_receive('execute_hook').never()
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {}
|
||||
|
||||
|
|
@ -33,7 +161,18 @@ def test_collect_configuration_run_summary_logs_info_for_success():
|
|||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_executes_hooks_for_create():
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
||||
|
|
@ -45,33 +184,74 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_extract():
|
|||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_extract_with_repository_error():
|
||||
def test_collect_configuration_run_summary_logs_extract_with_repository_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||
ValueError
|
||||
)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'extract': flexmock(repository='repo')}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_list_with_archive_and_repository_error():
|
||||
def test_collect_configuration_run_summary_logs_missing_configs_error():
|
||||
arguments = {'global': flexmock(config_paths=[])}
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
|
||||
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
|
||||
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_pre_hook_error():
|
||||
flexmock(module.command).should_receive('execute_hook').and_raise(ValueError)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_post_hook_error():
|
||||
flexmock(module.command).should_receive('execute_hook').and_return(None).and_raise(ValueError)
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'create': flexmock(), 'global': flexmock(dry_run=False)}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert expected_logs[0] in logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_for_list_with_archive_and_repository_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository').and_raise(
|
||||
ValueError
|
||||
)
|
||||
expected_logs = (flexmock(),)
|
||||
flexmock(module).should_receive('make_error_log_records').and_return(expected_logs)
|
||||
arguments = {'list': flexmock(repository='repo', archive='test')}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
assert logs == expected_logs
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
||||
|
|
@ -82,19 +262,21 @@ def test_collect_configuration_run_summary_logs_info_for_success_with_list():
|
|||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert all(log for log in logs if log.levelno == module.logging.INFO)
|
||||
assert {log.levelno for log in logs} == {logging.INFO}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_run_error():
|
||||
def test_collect_configuration_run_summary_logs_run_configuration_error():
|
||||
flexmock(module.validate).should_receive('guard_configuration_contains_repository')
|
||||
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
|
||||
flexmock(module).should_receive('run_configuration').and_return(
|
||||
[logging.makeLogRecord(dict(levelno=logging.CRITICAL, levelname='CRITICAL', msg='Error'))]
|
||||
)
|
||||
arguments = {}
|
||||
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs({'test.yaml': {}}, arguments=arguments)
|
||||
)
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
assert {log.levelno for log in logs} == {logging.CRITICAL}
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
||||
|
|
@ -109,12 +291,3 @@ def test_collect_configuration_run_summary_logs_outputs_merged_json_results():
|
|||
{'test.yaml': {}, 'test2.yaml': {}}, arguments=arguments
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
|
||||
flexmock(module).should_receive('run_configuration').and_return([])
|
||||
arguments = {'global': flexmock(config_paths=[])}
|
||||
|
||||
logs = tuple(module.collect_configuration_run_summary_logs({}, arguments=arguments))
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import generate as module
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_with_examples():
|
||||
def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||
flexmock(module).should_receive('add_comments_to_configuration')
|
||||
flexmock(module).should_receive('add_comments_to_configuration_map')
|
||||
schema = {
|
||||
'map': OrderedDict(
|
||||
[
|
||||
|
|
@ -35,3 +36,38 @@ def test_schema_to_sample_configuration_generates_config_with_examples():
|
|||
('section2', OrderedDict([('field2', 'Example 2'), ('field3', 'Example 3')])),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_strings_with_example():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
schema = {'seq': [{'type': 'str'}], 'example': ['hi']}
|
||||
|
||||
config = module._schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == ['hi']
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
schema = {
|
||||
'seq': [
|
||||
{
|
||||
'map': OrderedDict(
|
||||
[('field1', {'example': 'Example 1'}), ('field2', {'example': 'Example 2'})]
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
config = module._schema_to_sample_configuration(schema)
|
||||
|
||||
assert config == [OrderedDict([('field1', 'Example 1'), ('field2', 'Example 2')])]
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_with_unsupported_schema_raises():
|
||||
schema = {'gobbledygook': [{'type': 'not-your'}]}
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module._schema_to_sample_configuration(schema)
|
||||
|
|
|
|||
|
|
@ -74,6 +74,27 @@ def test_apply_logical_validation_does_not_raise_otherwise():
|
|||
module.apply_logical_validation('config.yaml', {'retention': {'keep_secondly': 1000}})
|
||||
|
||||
|
||||
def test_remove_examples_strips_examples_from_map():
|
||||
schema = {
|
||||
'map': {
|
||||
'foo': {'desc': 'thing1', 'example': 'bar'},
|
||||
'baz': {'desc': 'thing2', 'example': 'quux'},
|
||||
}
|
||||
}
|
||||
|
||||
module.remove_examples(schema)
|
||||
|
||||
assert schema == {'map': {'foo': {'desc': 'thing1'}, 'baz': {'desc': 'thing2'}}}
|
||||
|
||||
|
||||
def test_remove_examples_strips_examples_from_sequence_of_maps():
|
||||
schema = {'seq': [{'map': {'foo': {'desc': 'thing', 'example': 'bar'}}, 'example': 'stuff'}]}
|
||||
|
||||
module.remove_examples(schema)
|
||||
|
||||
assert schema == {'seq': [{'map': {'foo': {'desc': 'thing'}}}]}
|
||||
|
||||
|
||||
def test_guard_configuration_contains_repository_does_not_raise_when_repository_in_config():
|
||||
module.guard_configuration_contains_repository(
|
||||
repository='repo', configurations={'config.yaml': {'location': {'repositories': ['repo']}}}
|
||||
|
|
|
|||
0
tests/unit/hooks/__init__.py
Normal file
0
tests/unit/hooks/__init__.py
Normal file
|
|
@ -2,10 +2,27 @@ import logging
|
|||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic import hook as module
|
||||
from borgmatic.hooks import command as module
|
||||
|
||||
|
||||
def test_interpolate_context_passes_through_command_without_variable():
|
||||
assert module.interpolate_context('ls', {'foo': 'bar'}) == 'ls'
|
||||
|
||||
|
||||
def test_interpolate_context_passes_through_command_with_unknown_variable():
|
||||
assert module.interpolate_context('ls {baz}', {'foo': 'bar'}) == 'ls {baz}'
|
||||
|
||||
|
||||
def test_interpolate_context_interpolates_variables():
|
||||
context = {'foo': 'bar', 'baz': 'quux'}
|
||||
|
||||
assert module.interpolate_context('ls {foo}{baz} {baz}', context) == 'ls barquux quux'
|
||||
|
||||
|
||||
def test_execute_hook_invokes_each_command():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.WARNING, shell=True
|
||||
).once()
|
||||
|
|
@ -14,6 +31,9 @@ def test_execute_hook_invokes_each_command():
|
|||
|
||||
|
||||
def test_execute_hook_with_multiple_commands_invokes_each_command():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.WARNING, shell=True
|
||||
).once()
|
||||
|
|
@ -25,6 +45,9 @@ def test_execute_hook_with_multiple_commands_invokes_each_command():
|
|||
|
||||
|
||||
def test_execute_hook_with_umask_sets_that_umask():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.os).should_receive('umask').with_args(0o77).and_return(0o22).once()
|
||||
flexmock(module.os).should_receive('umask').with_args(0o22).once()
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
|
|
@ -35,6 +58,9 @@ def test_execute_hook_with_umask_sets_that_umask():
|
|||
|
||||
|
||||
def test_execute_hook_with_dry_run_skips_commands():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').never()
|
||||
|
||||
module.execute_hook([':', 'true'], None, 'config.yaml', 'pre-backup', dry_run=True)
|
||||
|
|
@ -45,6 +71,9 @@ def test_execute_hook_with_empty_commands_does_not_raise():
|
|||
|
||||
|
||||
def test_execute_hook_on_error_logs_as_error():
|
||||
flexmock(module).should_receive('interpolate_context').replace_with(
|
||||
lambda command, context: command
|
||||
)
|
||||
flexmock(module.execute).should_receive('execute_command').with_args(
|
||||
[':'], output_log_level=logging.ERROR, shell=True
|
||||
).once()
|
||||
33
tests/unit/hooks/test_healthchecks.py
Normal file
33
tests/unit/hooks/test_healthchecks.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import healthchecks as module
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url():
|
||||
ping_url = 'https://example.com'
|
||||
flexmock(module.requests).should_receive('get').with_args(ping_url)
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_without_ping_url_does_not_raise():
|
||||
flexmock(module.requests).should_receive('get').never()
|
||||
|
||||
module.ping_healthchecks(ping_url_or_uuid=None, config_filename='config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_with_ping_uuid_hits_corresponding_url():
|
||||
ping_uuid = 'abcd-efgh-ijkl-mnop'
|
||||
flexmock(module.requests).should_receive('get').with_args(
|
||||
'https://hc-ping.com/{}'.format(ping_uuid)
|
||||
)
|
||||
|
||||
module.ping_healthchecks(ping_uuid, 'config.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_ping_healthchecks_hits_ping_url_with_append():
|
||||
ping_url = 'https://example.com'
|
||||
append = 'failed-so-hard'
|
||||
flexmock(module.requests).should_receive('get').with_args('{}/{}'.format(ping_url, append))
|
||||
|
||||
module.ping_healthchecks(ping_url, 'config.yaml', dry_run=False, append=append)
|
||||
187
tests/unit/hooks/test_postgresql.py
Normal file
187
tests/unit/hooks/test_postgresql.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import postgresql as module
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/{}'.format(name),
|
||||
'--format',
|
||||
'custom',
|
||||
name,
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_with_dry_run_skips_pg_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_dump_databases_without_databases_does_not_raise():
|
||||
module.dump_databases([], 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_with_invalid_database_name_raises():
|
||||
databases = [{'name': 'heehee/../../etc/passwd'}]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_hostname_and_port():
|
||||
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/database.example.org/foo',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--format',
|
||||
'custom',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_username_and_password():
|
||||
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--username',
|
||||
'postgres',
|
||||
'--format',
|
||||
'custom',
|
||||
'foo',
|
||||
),
|
||||
extra_environment={'PGPASSWORD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_format():
|
||||
databases = [{'name': 'foo', 'format': 'tar'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--format',
|
||||
'tar',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_with_options():
|
||||
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'pg_dump',
|
||||
'--no-password',
|
||||
'--clean',
|
||||
'--file',
|
||||
'databases/localhost/foo',
|
||||
'--format',
|
||||
'custom',
|
||||
'--stuff=such',
|
||||
'foo',
|
||||
),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dumpall_for_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('makedirs')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('pg_dumpall', '--no-password', '--clean', '--file', 'databases/localhost/all'),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.dump_databases(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_removes_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
flexmock(module.os).should_receive('listdir').and_return([])
|
||||
flexmock(module.os).should_receive('rmdir')
|
||||
|
||||
for name in ('foo', 'bar'):
|
||||
flexmock(module.os).should_receive('remove').with_args(
|
||||
'databases/localhost/{}'.format(name)
|
||||
).once()
|
||||
|
||||
module.remove_database_dumps(databases, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_with_dry_run_skips_removal():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module.os).should_receive('remove').never()
|
||||
|
||||
module.remove_database_dumps(databases, 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_remove_database_dumps_without_databases_does_not_raise():
|
||||
module.remove_database_dumps([], 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_with_invalid_database_name_raises():
|
||||
databases = [{'name': 'heehee/../../etc/passwd'}]
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.remove_database_dumps(databases, 'test.yaml', dry_run=True)
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic import execute as module
|
||||
|
|
@ -7,8 +8,9 @@ from borgmatic import execute as module
|
|||
|
||||
def test_execute_command_calls_full_command():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command, output_log_level=logging.INFO, shell=False
|
||||
full_command, output_log_level=logging.INFO, shell=False, environment=None
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command)
|
||||
|
|
@ -18,8 +20,9 @@ def test_execute_command_calls_full_command():
|
|||
|
||||
def test_execute_command_calls_full_command_with_shell():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command, output_log_level=logging.INFO, shell=True
|
||||
full_command, output_log_level=logging.INFO, shell=True, environment=None
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, shell=True)
|
||||
|
|
@ -27,11 +30,24 @@ def test_execute_command_calls_full_command_with_shell():
|
|||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_calls_full_command_with_extra_environment():
|
||||
full_command = ['foo', 'bar']
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module).should_receive('execute_and_log_output').with_args(
|
||||
full_command, output_log_level=logging.INFO, shell=False, environment={'a': 'b', 'c': 'd'}
|
||||
).once()
|
||||
|
||||
output = module.execute_command(full_command, extra_environment={'c': 'd'})
|
||||
|
||||
assert output is None
|
||||
|
||||
|
||||
def test_execute_command_captures_output():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=False
|
||||
full_command, shell=False, env=None
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(full_command, output_log_level=None)
|
||||
|
|
@ -42,10 +58,51 @@ def test_execute_command_captures_output():
|
|||
def test_execute_command_captures_output_with_shell():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=True
|
||||
full_command, shell=True, env=None
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(full_command, output_log_level=None, shell=True)
|
||||
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_captures_output_with_extra_environment():
|
||||
full_command = ['foo', 'bar']
|
||||
expected_output = '[]'
|
||||
flexmock(module.os, environ={'a': 'b'})
|
||||
flexmock(module.subprocess).should_receive('check_output').with_args(
|
||||
full_command, shell=False, env={'a': 'b', 'c': 'd'}
|
||||
).and_return(flexmock(decode=lambda: expected_output)).once()
|
||||
|
||||
output = module.execute_command(
|
||||
full_command, output_log_level=None, shell=False, extra_environment={'c': 'd'}
|
||||
)
|
||||
|
||||
assert output == expected_output
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_success():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(0, 'borg init')
|
||||
)
|
||||
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
||||
|
||||
def test_execute_command_without_capture_does_not_raise_on_warning():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(1, 'borg init')
|
||||
)
|
||||
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
||||
|
||||
def test_execute_command_without_capture_raises_on_error():
|
||||
flexmock(module.subprocess).should_receive('check_call').and_raise(
|
||||
module.subprocess.CalledProcessError(2, 'borg init')
|
||||
)
|
||||
|
||||
with pytest.raises(module.subprocess.CalledProcessError):
|
||||
module.execute_command_without_capture(('borg', 'init'))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,29 @@ def test_to_bool_passes_none_through():
|
|||
assert module.to_bool(None) is None
|
||||
|
||||
|
||||
def test_interactive_console_false_when_not_isatty(capsys):
|
||||
with capsys.disabled():
|
||||
flexmock(module.sys.stdout).should_receive('isatty').and_return(False)
|
||||
|
||||
assert module.interactive_console() is False
|
||||
|
||||
|
||||
def test_interactive_console_false_when_TERM_is_dumb(capsys):
|
||||
with capsys.disabled():
|
||||
flexmock(module.sys.stdout).should_receive('isatty').and_return(True)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TERM').and_return('dumb')
|
||||
|
||||
assert module.interactive_console() is False
|
||||
|
||||
|
||||
def test_interactive_console_true_when_isatty_and_TERM_is_not_dumb(capsys):
|
||||
with capsys.disabled():
|
||||
flexmock(module.sys.stdout).should_receive('isatty').and_return(True)
|
||||
flexmock(module.os.environ).should_receive('get').with_args('TERM').and_return('smart')
|
||||
|
||||
assert module.interactive_console() is True
|
||||
|
||||
|
||||
def test_should_do_markup_respects_no_color_value():
|
||||
assert module.should_do_markup(no_color=True, configs={}) is False
|
||||
|
||||
|
|
@ -75,15 +98,17 @@ def test_should_do_markup_prefers_no_color_value_to_PY_COLORS():
|
|||
assert module.should_do_markup(no_color=True, configs={}) is False
|
||||
|
||||
|
||||
def test_should_do_markup_respects_stdout_tty_value():
|
||||
def test_should_do_markup_respects_interactive_console_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return(None)
|
||||
flexmock(module).should_receive('interactive_console').and_return(True)
|
||||
|
||||
assert module.should_do_markup(no_color=False, configs={}) is False
|
||||
assert module.should_do_markup(no_color=False, configs={}) is True
|
||||
|
||||
|
||||
def test_should_do_markup_prefers_PY_COLORS_to_stdout_tty_value():
|
||||
def test_should_do_markup_prefers_PY_COLORS_to_interactive_console_value():
|
||||
flexmock(module.os.environ).should_receive('get').and_return('True')
|
||||
flexmock(module).should_receive('to_bool').and_return(True)
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
|
||||
assert module.should_do_markup(no_color=False, configs={}) is True
|
||||
|
||||
|
| ||||