mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-26 19:33:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb353d358 | ||
|
|
3b99f7c75a | ||
|
|
8b9abc6cf8 | ||
|
|
da034c316a | ||
|
|
08d01d8bcd | ||
|
|
eef69e23ee | ||
|
|
26bb54a9dd | ||
|
|
715e2ac127 | ||
|
|
f39cea4abf | ||
|
|
22101bdd49 | ||
|
|
13cf863d89 | ||
|
|
dcf25fa041 | ||
|
|
12b75f9075 | ||
|
|
9baf06a2f7 | ||
|
|
56302e22cd | ||
|
|
6cc93c4eb9 | ||
|
|
2da43239f6 | ||
|
|
4beef36d3c | ||
|
|
eacfbd742b | ||
|
|
82a85986b6 | ||
|
|
ef448e2dd1 | ||
|
|
c3efe1b90e | ||
|
|
d85c1ee216 | ||
|
|
b47088067c | ||
|
|
c5732aa4fc | ||
|
|
a0323d9d6c | ||
|
|
8ad7b473f1 | ||
|
|
895a0ccb3c | ||
|
|
257ab77bea | ||
|
|
dccaa4014b | ||
|
|
2f3c0bec5b | ||
|
|
487d8ffd32 | ||
|
|
30523a7c89 | ||
|
|
77b1907d03 | ||
|
|
09594c85bf | ||
|
|
e07efdf68f | ||
|
|
1fed44f905 | ||
|
|
c687dafdd2 | ||
|
|
3eff2c4248 | ||
|
|
d94fdb6faf | ||
|
|
a83282faf0 | ||
|
|
e7169f6fb2 | ||
|
|
9587fc2366 | ||
|
|
5f06884d5a | ||
|
|
f011431463 | ||
|
|
9e14f209f1 | ||
|
|
9d34d2eec5 | ||
|
|
7a9625cd44 | ||
|
|
4763c323d0 | ||
|
|
eaa22be3db | ||
|
|
a587e207f9 | ||
|
|
db8079b699 | ||
|
|
5a989826a1 | ||
|
|
21f4266273 | ||
|
|
e7252c7545 | ||
|
|
86011c8418 | ||
|
|
f3295ccb4a | ||
|
|
cacb81f086 | ||
|
|
06c2154e6a | ||
|
|
ac1e1a9407 | ||
|
|
10933fd55b | ||
|
|
af422ad705 | ||
|
|
d9d35491fb | ||
|
|
b540e63c0e | ||
|
|
5a56208922 | ||
|
|
5912769273 | ||
|
|
bac2aabe66 | ||
|
|
9f3328781b | ||
|
|
0205748db8 | ||
|
|
d0a8251ad2 | ||
|
|
32019ea8f3 | ||
|
|
fa9a061033 | ||
|
|
b3d2560563 | ||
|
|
4b4f56da42 | ||
|
|
b96d1898f7 | ||
|
|
099a712e53 | ||
|
|
9e2674ea5a | ||
|
|
7e419ec995 | ||
|
|
a3edf757ee | ||
|
|
e576403b64 | ||
|
|
7313430178 | ||
|
|
962daaa8b9 | ||
|
|
cd51e9c1ea | ||
|
|
6dca7c1c15 | ||
|
|
fd8c56c6be | ||
|
|
065057c966 | ||
|
|
c04517f843 | ||
|
|
5d80c366fb | ||
|
|
193dd93de2 | ||
|
|
8a94b9e2f1 | ||
|
|
3c8f6040e2 | ||
|
|
14e2a6b89d | ||
|
|
e607de7df1 | ||
|
|
e9bd5f4e1d | ||
|
|
175003ff9b | ||
|
|
b8d349d048 | ||
|
|
f6f06551f0 | ||
|
|
69771fe7ce | ||
|
|
c5c3e2e0ce | ||
|
|
8491b2c416 | ||
|
|
962267b3c5 | ||
|
|
562f4a281b | ||
|
|
6b09ca8022 | ||
|
|
f2ce2f387f | ||
|
|
782a9bb70a | ||
|
|
88adb5b3de | ||
|
|
59465b256d | ||
|
|
adfb89ee65 | ||
|
|
c11dcdef0a | ||
|
|
8a2514915c | ||
|
|
4d7a2876a5 | ||
|
|
309f67e860 | ||
|
|
1f415a24b8 |
+20
-11
@@ -13,19 +13,30 @@ services:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: test2
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: postgres2
|
||||
commands:
|
||||
- docker-entrypoint.sh -p 5433
|
||||
- name: mysql
|
||||
image: docker.io/mariadb:10.5
|
||||
- name: mariadb
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: test
|
||||
MYSQL_DATABASE: test
|
||||
- name: mysql2
|
||||
image: docker.io/mariadb:10.5
|
||||
MARIADB_ROOT_PASSWORD: test
|
||||
MARIADB_DATABASE: test
|
||||
- name: mariadb2
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: test2
|
||||
MYSQL_DATABASE: test
|
||||
MARIADB_ROOT_PASSWORD: test2
|
||||
MARIADB_DATABASE: test
|
||||
commands:
|
||||
- docker-entrypoint.sh --port=3307
|
||||
- name: not-actually-mysql
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: test
|
||||
MARIADB_DATABASE: test
|
||||
- name: not-actually-mysql2
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: test2
|
||||
MARIADB_DATABASE: test
|
||||
commands:
|
||||
- docker-entrypoint.sh --port=3307
|
||||
- name: mongodb
|
||||
@@ -82,5 +93,3 @@ trigger:
|
||||
- borgmatic-collective/borgmatic
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
@@ -1 +1 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
|
||||
@@ -1,3 +1,81 @@
|
||||
1.8.5
|
||||
* #701: Add a "skip_actions" option to skip running particular actions, handy for append-only or
|
||||
checkless configurations. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#skipping-actions
|
||||
* #701: Deprecate the "disabled" value for the "checks" option in favor of the new "skip_actions"
|
||||
option.
|
||||
* #745: Constants now apply to included configuration, not just the file doing the includes. As a
|
||||
side effect of this change, constants no longer apply to option names and only substitute into
|
||||
configuration values.
|
||||
* #779: Add a "--match-archives" flag to the "check" action for selecting the archives to check,
|
||||
overriding the existing "archive_name_format" and "match_archives" options in configuration.
|
||||
* #779: Only parse "--override" values as complex data types when they're for options of those
|
||||
types.
|
||||
* #782: Fix environment variable interpolation within configured repository paths.
|
||||
* #782: Add configuration constant overriding via the existing "--override" flag.
|
||||
* #783: Upgrade ruamel.yaml dependency to support version 0.18.x.
|
||||
* #784: Drop support for Python 3.7, which has been end-of-lifed.
|
||||
|
||||
1.8.4
|
||||
* #715: Add a monitoring hook for sending backup status to a variety of monitoring services via the
|
||||
Apprise library. See the documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook
|
||||
* #748: When an archive filter causes no matching archives for the "rlist" or "info"
|
||||
actions, warn the user and suggest how to remove the filter.
|
||||
* #768: Fix a traceback when an invalid command-line flag or action is used.
|
||||
* #771: Fix normalization of deprecated sections ("location:", "storage:", "hooks:", etc.) to
|
||||
support empty sections without erroring.
|
||||
* #774: Disallow the "--dry-run" flag with the "borg" action, as borgmatic can't guarantee the Borg
|
||||
command won't have side effects.
|
||||
|
||||
1.8.3
|
||||
* #665: BREAKING: Simplify logging logic as follows: Syslog verbosity is now disabled by
|
||||
default, but setting the "--syslog-verbosity" flag enables it regardless of whether you're at an
|
||||
interactive console. Additionally, "--log-file-verbosity" and "--monitoring-verbosity" now
|
||||
default to 1 (info about steps borgmatic is taking) instead of 0. And both syslog logging and
|
||||
file logging can be enabled simultaneously.
|
||||
* #743: Add a monitoring hook for sending backup status and logs to Grafana Loki. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook
|
||||
* #753: When "archive_name_format" is not set, filter archives using the default archive name
|
||||
format.
|
||||
* #754: Fix error handling to log command output as one record per line instead of truncating
|
||||
too-long output and swallowing the end of some Borg error messages.
|
||||
* #757: Update documentation so "sudo borgmatic" works for pipx borgmatic installations.
|
||||
* #761: Fix for borgmatic not stopping Borg immediately when the user presses ctrl-C.
|
||||
* Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip. See the
|
||||
documentation for more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation
|
||||
https://torsion.org/borgmatic/docs/how-to/upgrade/#upgrading-borgmatic
|
||||
|
||||
1.8.2
|
||||
* #345: Add "key export" action to export a copy of the repository key for safekeeping in case
|
||||
the original goes missing or gets damaged.
|
||||
* #727: Add a MariaDB database hook that uses native MariaDB commands instead of the deprecated
|
||||
MySQL ones. Be aware though that any existing backups made with the "mysql_databases:" hook are
|
||||
only restorable with a "mysql_databases:" configuration.
|
||||
* #738: Fix for potential data loss (data not getting restored) in which the database "restore"
|
||||
action didn't actually restore anything and indicated success anyway.
|
||||
* Remove the deprecated use of the MongoDB hook's "--db" flag for database restoration.
|
||||
* Add source code reference documentation for getting oriented with the borgmatic code as a
|
||||
developer: https://torsion.org/borgmatic/docs/reference/source-code/
|
||||
|
||||
1.8.1
|
||||
* #326: Add documentation for restoring a database to an alternate host:
|
||||
https://torsion.org/borgmatic/docs/how-to/backup-your-databases/#restore-to-an-alternate-host
|
||||
* #697: Add documentation for "bootstrap" action:
|
||||
https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive
|
||||
* #725: Add "store_config_files" option for disabling the automatic backup of configuration files
|
||||
used by the "config bootstrap" action.
|
||||
* #728: Fix for "prune" action error when using the "keep_exclude_tags" option.
|
||||
* #730: Fix for Borg's interactive prompt on the "check --repair" action automatically getting
|
||||
answered "NO" even when the "check_i_know_what_i_am_doing" option isn't set.
|
||||
* #732: Include multiple configuration files with a single "!include". See the documentation for
|
||||
more information:
|
||||
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#multiple-merge-includes
|
||||
* #734: Omit "--glob-archives" or "--match-archives" Borg flag when its value would be "*" (meaning
|
||||
all archives).
|
||||
|
||||
1.8.0
|
||||
* #575: BREAKING: For the "borgmatic borg" action, instead of implicitly injecting
|
||||
repository/archive into the resulting Borg command-line, pass repository to Borg via an
|
||||
@@ -10,10 +88,10 @@
|
||||
"check --repair".
|
||||
* When merging two configuration files, error gracefully if the two files do not adhere to the same
|
||||
format.
|
||||
* #721: Remove configuration sections ("location:", "storage:", "hooks:" etc.), while still keeping
|
||||
deprecated support for them. Now, all options are at the same level, and you don't need to worry
|
||||
about commenting/uncommenting section headers when you change an option (if you remove your
|
||||
sections first).
|
||||
* #721: Remove configuration sections ("location:", "storage:", "hooks:", etc.), while still
|
||||
keeping deprecated support for them. Now, all options are at the same level, and you don't need
|
||||
to worry about commenting/uncommenting section headers when you change an option (if you remove
|
||||
your sections first).
|
||||
* #721: BREAKING: The retention prefix and the consistency prefix can no longer have different
|
||||
values (unless one is not set).
|
||||
* #721: BREAKING: The storage umask and the hooks umask can no longer have different values (unless
|
||||
|
||||
@@ -48,24 +48,27 @@ postgresql_databases:
|
||||
- name: users
|
||||
|
||||
# Third-party services to notify you if backups aren't happening.
|
||||
healthchecks: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
|
||||
healthchecks:
|
||||
ping_url: https://hc-ping.com/be067061-cf96-4412-8eae-62b0c50d6a8c
|
||||
```
|
||||
|
||||
borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
|
||||
|
||||
## Integrations
|
||||
|
||||
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px;"></a>
|
||||
<a href="https://www.postgresql.org/"><img src="docs/static/postgresql.png" alt="PostgreSQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.mysql.com/"><img src="docs/static/mysql.png" alt="MySQL" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://mariadb.com/"><img src="docs/static/mariadb.png" alt="MariaDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://cronitor.io/"><img src="docs/static/cronitor.png" alt="Cronitor" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://cronhub.io/"><img src="docs/static/cronhub.png" alt="Cronhub" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.pagerduty.com/"><img src="docs/static/pagerduty.png" alt="PagerDuty" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://ntfy.sh/"><img src="docs/static/ntfy.png" alt="ntfy" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://grafana.com/oss/loki/"><img src="docs/static/loki.png" alt="Loki" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://github.com/caronc/apprise/wiki"><img src="docs/static/apprise.png" alt="Apprise" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
<a href="https://www.borgbase.com/?utm_source=borgmatic"><img src="docs/static/borgbase.png" alt="BorgBase" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -39,13 +39,10 @@ def run_check(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
check_arguments,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
progress=check_arguments.progress,
|
||||
repair=check_arguments.repair,
|
||||
only_checks=check_arguments.only,
|
||||
force=check_arguments.force,
|
||||
)
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
config.get('after_check'),
|
||||
|
||||
+12
-13
@@ -1,12 +1,8 @@
|
||||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
try:
|
||||
import importlib_metadata
|
||||
except ModuleNotFoundError: # pragma: nocover
|
||||
import importlib.metadata as importlib_metadata
|
||||
|
||||
import borgmatic.borg.create
|
||||
import borgmatic.borg.state
|
||||
import borgmatic.config.validate
|
||||
@@ -39,7 +35,7 @@ def create_borgmatic_manifest(config, config_paths, dry_run):
|
||||
with open(borgmatic_manifest_path, 'w') as config_list_file:
|
||||
json.dump(
|
||||
{
|
||||
'borgmatic_version': importlib_metadata.version('borgmatic'),
|
||||
'borgmatic_version': importlib.metadata.version('borgmatic'),
|
||||
'config_paths': config_paths,
|
||||
},
|
||||
config_list_file,
|
||||
@@ -78,20 +74,23 @@ def run_create(
|
||||
)
|
||||
logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}')
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||
'dump_databases',
|
||||
'dump_data_sources',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
create_borgmatic_manifest(config, global_arguments.used_config_paths, global_arguments.dry_run)
|
||||
if config.get('store_config_files', True):
|
||||
create_borgmatic_manifest(
|
||||
config, global_arguments.used_config_paths, global_arguments.dry_run
|
||||
)
|
||||
stream_processes = [process for processes in active_dumps.values() for process in processes]
|
||||
|
||||
json_output = borgmatic.borg.create.create_archive(
|
||||
@@ -112,10 +111,10 @@ def run_create(
|
||||
yield json.loads(json_output)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
config_filename,
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
borgmatic.hooks.command.execute_hook(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import logging
|
||||
|
||||
import borgmatic.borg.export_key
|
||||
import borgmatic.config.validate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Run the "key export" action for the given repository.
|
||||
'''
|
||||
if export_arguments.repository is None or borgmatic.config.validate.repositories_match(
|
||||
repository, export_arguments.repository
|
||||
):
|
||||
logger.info(f'{repository.get("label", repository["path"])}: Exporting repository key')
|
||||
borgmatic.borg.export_key.export_key(
|
||||
repository['path'],
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
+123
-115
@@ -17,50 +17,51 @@ logger = logging.getLogger(__name__)
|
||||
UNSPECIFIED_HOOK = object()
|
||||
|
||||
|
||||
def get_configured_database(
|
||||
config, archive_database_names, hook_name, database_name, configuration_database_name=None
|
||||
def get_configured_data_source(
|
||||
config,
|
||||
archive_data_source_names,
|
||||
hook_name,
|
||||
data_source_name,
|
||||
configuration_data_source_name=None,
|
||||
):
|
||||
'''
|
||||
Find the first database with the given hook name and database name in the configuration dict and
|
||||
the given archive database names dict (from hook name to database names contained in a
|
||||
particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all database
|
||||
hooks for the named database. If a configuration database name is given, use that instead of the
|
||||
database name to lookup the database in the given hooks configuration.
|
||||
Find the first data source with the given hook name and data source name in the configuration
|
||||
dict and the given archive data source names dict (from hook name to data source names contained
|
||||
in a particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all data
|
||||
source hooks for the named data source. If a configuration data source name is given, use that
|
||||
instead of the data source name to lookup the data source in the given hooks configuration.
|
||||
|
||||
Return the found database as a tuple of (found hook name, database configuration dict).
|
||||
Return the found data source as a tuple of (found hook name, data source configuration dict) or
|
||||
(None, None) if not found.
|
||||
'''
|
||||
if not configuration_database_name:
|
||||
configuration_database_name = database_name
|
||||
if not configuration_data_source_name:
|
||||
configuration_data_source_name = data_source_name
|
||||
|
||||
if hook_name == UNSPECIFIED_HOOK:
|
||||
hooks_to_search = {
|
||||
hook_name: value
|
||||
for (hook_name, value) in config.items()
|
||||
if hook_name in borgmatic.hooks.dump.DATABASE_HOOK_NAMES
|
||||
if hook_name in borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES
|
||||
}
|
||||
else:
|
||||
hooks_to_search = {hook_name: config[hook_name]}
|
||||
try:
|
||||
hooks_to_search = {hook_name: config[hook_name]}
|
||||
except KeyError:
|
||||
return (None, None)
|
||||
|
||||
return next(
|
||||
(
|
||||
(name, hook_database)
|
||||
(name, hook_data_source)
|
||||
for (name, hook) in hooks_to_search.items()
|
||||
for hook_database in hook
|
||||
if hook_database['name'] == configuration_database_name
|
||||
and database_name in archive_database_names.get(name, [])
|
||||
for hook_data_source in hook
|
||||
if hook_data_source['name'] == configuration_data_source_name
|
||||
and data_source_name in archive_data_source_names.get(name, [])
|
||||
),
|
||||
(None, None),
|
||||
)
|
||||
|
||||
|
||||
def get_configured_hook_name_and_database(hooks, database_name):
|
||||
'''
|
||||
Find the hook name and first database dict with the given database name in the configured hooks
|
||||
dict. This searches across all database hooks.
|
||||
'''
|
||||
|
||||
|
||||
def restore_single_database(
|
||||
def restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
@@ -69,27 +70,27 @@ def restore_single_database(
|
||||
remote_path,
|
||||
archive_name,
|
||||
hook_name,
|
||||
database,
|
||||
data_source,
|
||||
connection_params,
|
||||
): # pragma: no cover
|
||||
'''
|
||||
Given (among other things) an archive name, a database hook name, the hostname,
|
||||
port, username and password as connection params, and a configured database
|
||||
configuration dict, restore that database from the archive.
|
||||
Given (among other things) an archive name, a data source hook name, the hostname, port,
|
||||
username/password as connection params, and a configured data source configuration dict, restore
|
||||
that data source from the archive.
|
||||
'''
|
||||
logger.info(
|
||||
f'{repository.get("label", repository["path"])}: Restoring database {database["name"]}'
|
||||
f'{repository.get("label", repository["path"])}: Restoring data source {data_source["name"]}'
|
||||
)
|
||||
|
||||
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
|
||||
'make_database_dump_pattern',
|
||||
'make_data_source_dump_pattern',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
database['name'],
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
data_source['name'],
|
||||
)[hook_name]
|
||||
|
||||
# Kick off a single database extract to stdout.
|
||||
# Kick off a single data source extract to stdout.
|
||||
extract_process = borgmatic.borg.extract.extract_archive(
|
||||
dry_run=global_arguments.dry_run,
|
||||
repository=repository['path'],
|
||||
@@ -103,23 +104,23 @@ def restore_single_database(
|
||||
destination_path='/',
|
||||
# A directory format dump isn't a single file, and therefore can't extract
|
||||
# to stdout. In this case, the extract_process return value is None.
|
||||
extract_to_stdout=bool(database.get('format') != 'directory'),
|
||||
extract_to_stdout=bool(data_source.get('format') != 'directory'),
|
||||
)
|
||||
|
||||
# Run a single database restore, consuming the extract stdout (if any).
|
||||
# Run a single data source restore, consuming the extract stdout (if any).
|
||||
borgmatic.hooks.dispatch.call_hooks(
|
||||
'restore_database_dump',
|
||||
config,
|
||||
repository['path'],
|
||||
database['name'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
extract_process,
|
||||
connection_params,
|
||||
function_name='restore_data_source_dump',
|
||||
config=config,
|
||||
log_prefix=repository['path'],
|
||||
hook_names=[hook_name],
|
||||
data_source=data_source,
|
||||
dry_run=global_arguments.dry_run,
|
||||
extract_process=extract_process,
|
||||
connection_params=connection_params,
|
||||
)
|
||||
|
||||
|
||||
def collect_archive_database_names(
|
||||
def collect_archive_data_source_names(
|
||||
repository,
|
||||
archive,
|
||||
config,
|
||||
@@ -131,60 +132,62 @@ def collect_archive_database_names(
|
||||
'''
|
||||
Given a local or remote repository path, a resolved archive name, a configuration dict, the
|
||||
local Borg version, global_arguments an argparse.Namespace, and local and remote Borg paths,
|
||||
query the archive for the names of databases it contains and return them as a dict from hook
|
||||
name to a sequence of database names.
|
||||
query the archive for the names of data sources it contains as dumps and return them as a dict
|
||||
from hook name to a sequence of data source names.
|
||||
'''
|
||||
borgmatic_source_directory = os.path.expanduser(
|
||||
config.get(
|
||||
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||
)
|
||||
).lstrip('/')
|
||||
parent_dump_path = os.path.expanduser(
|
||||
borgmatic.hooks.dump.make_database_dump_path(borgmatic_source_directory, '*_databases/*/*')
|
||||
)
|
||||
dump_paths = borgmatic.borg.list.capture_archive_listing(
|
||||
repository,
|
||||
archive,
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
list_path=parent_dump_path,
|
||||
list_paths=[
|
||||
os.path.expanduser(
|
||||
borgmatic.hooks.dump.make_data_source_dump_path(borgmatic_source_directory, pattern)
|
||||
)
|
||||
for pattern in ('*_databases/*/*',)
|
||||
],
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
)
|
||||
|
||||
# Determine the database names corresponding to the dumps found in the archive and
|
||||
# Determine the data source names corresponding to the dumps found in the archive and
|
||||
# add them to restore_names.
|
||||
archive_database_names = {}
|
||||
archive_data_source_names = {}
|
||||
|
||||
for dump_path in dump_paths:
|
||||
try:
|
||||
(hook_name, _, database_name) = dump_path.split(
|
||||
(hook_name, _, data_source_name) = dump_path.split(
|
||||
borgmatic_source_directory + os.path.sep, 1
|
||||
)[1].split(os.path.sep)[0:3]
|
||||
except (ValueError, IndexError):
|
||||
logger.warning(
|
||||
f'{repository}: Ignoring invalid database dump path "{dump_path}" in archive {archive}'
|
||||
f'{repository}: Ignoring invalid data source dump path "{dump_path}" in archive {archive}'
|
||||
)
|
||||
else:
|
||||
if database_name not in archive_database_names.get(hook_name, []):
|
||||
archive_database_names.setdefault(hook_name, []).extend([database_name])
|
||||
if data_source_name not in archive_data_source_names.get(hook_name, []):
|
||||
archive_data_source_names.setdefault(hook_name, []).extend([data_source_name])
|
||||
|
||||
return archive_database_names
|
||||
return archive_data_source_names
|
||||
|
||||
|
||||
def find_databases_to_restore(requested_database_names, archive_database_names):
|
||||
def find_data_sources_to_restore(requested_data_source_names, archive_data_source_names):
|
||||
'''
|
||||
Given a sequence of requested database names to restore and a dict of hook name to the names of
|
||||
databases found in an archive, return an expanded sequence of database names to restore,
|
||||
replacing "all" with actual database names as appropriate.
|
||||
Given a sequence of requested data source names to restore and a dict of hook name to the names
|
||||
of data sources found in an archive, return an expanded sequence of data source names to
|
||||
restore, replacing "all" with actual data source names as appropriate.
|
||||
|
||||
Raise ValueError if any of the requested database names cannot be found in the archive.
|
||||
Raise ValueError if any of the requested data source names cannot be found in the archive.
|
||||
'''
|
||||
# A map from database hook name to the database names to restore for that hook.
|
||||
# A map from data source hook name to the data source names to restore for that hook.
|
||||
restore_names = (
|
||||
{UNSPECIFIED_HOOK: requested_database_names}
|
||||
if requested_database_names
|
||||
{UNSPECIFIED_HOOK: requested_data_source_names}
|
||||
if requested_data_source_names
|
||||
else {UNSPECIFIED_HOOK: ['all']}
|
||||
)
|
||||
|
||||
@@ -193,56 +196,59 @@ def find_databases_to_restore(requested_database_names, archive_database_names):
|
||||
if 'all' in restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names[UNSPECIFIED_HOOK].remove('all')
|
||||
|
||||
for hook_name, database_names in archive_database_names.items():
|
||||
restore_names.setdefault(hook_name, []).extend(database_names)
|
||||
for hook_name, data_source_names in archive_data_source_names.items():
|
||||
restore_names.setdefault(hook_name, []).extend(data_source_names)
|
||||
|
||||
# If a database is to be restored as part of "all", then remove it from restore names so
|
||||
# it doesn't get restored twice.
|
||||
for database_name in database_names:
|
||||
if database_name in restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names[UNSPECIFIED_HOOK].remove(database_name)
|
||||
# If a data source is to be restored as part of "all", then remove it from restore names
|
||||
# so it doesn't get restored twice.
|
||||
for data_source_name in data_source_names:
|
||||
if data_source_name in restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names[UNSPECIFIED_HOOK].remove(data_source_name)
|
||||
|
||||
if not restore_names[UNSPECIFIED_HOOK]:
|
||||
restore_names.pop(UNSPECIFIED_HOOK)
|
||||
|
||||
combined_restore_names = set(
|
||||
name for database_names in restore_names.values() for name in database_names
|
||||
name for data_source_names in restore_names.values() for name in data_source_names
|
||||
)
|
||||
combined_archive_database_names = set(
|
||||
name for database_names in archive_database_names.values() for name in database_names
|
||||
combined_archive_data_source_names = set(
|
||||
name
|
||||
for data_source_names in archive_data_source_names.values()
|
||||
for name in data_source_names
|
||||
)
|
||||
|
||||
missing_names = sorted(set(combined_restore_names) - combined_archive_database_names)
|
||||
missing_names = sorted(set(combined_restore_names) - combined_archive_data_source_names)
|
||||
if missing_names:
|
||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||
raise ValueError(
|
||||
f"Cannot restore database{'s' if len(missing_names) > 1 else ''} {joined_names} missing from archive"
|
||||
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from archive"
|
||||
)
|
||||
|
||||
return restore_names
|
||||
|
||||
|
||||
def ensure_databases_found(restore_names, remaining_restore_names, found_names):
|
||||
def ensure_data_sources_found(restore_names, remaining_restore_names, found_names):
|
||||
'''
|
||||
Given a dict from hook name to database names to restore, a dict from hook name to remaining
|
||||
database names to restore, and a sequence of found (actually restored) database names, raise
|
||||
ValueError if requested databases to restore were missing from the archive and/or configuration.
|
||||
Given a dict from hook name to data source names to restore, a dict from hook name to remaining
|
||||
data source names to restore, and a sequence of found (actually restored) data source names,
|
||||
raise ValueError if requested data source to restore were missing from the archive and/or
|
||||
configuration.
|
||||
'''
|
||||
combined_restore_names = set(
|
||||
name
|
||||
for database_names in tuple(restore_names.values())
|
||||
for data_source_names in tuple(restore_names.values())
|
||||
+ tuple(remaining_restore_names.values())
|
||||
for name in database_names
|
||||
for name in data_source_names
|
||||
)
|
||||
|
||||
if not combined_restore_names and not found_names:
|
||||
raise ValueError('No databases were found to restore')
|
||||
raise ValueError('No data sources were found to restore')
|
||||
|
||||
missing_names = sorted(set(combined_restore_names) - set(found_names))
|
||||
if missing_names:
|
||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||
raise ValueError(
|
||||
f"Cannot restore database{'s' if len(missing_names) > 1 else ''} {joined_names} missing from borgmatic's configuration"
|
||||
f"Cannot restore data source{'s' if len(missing_names) > 1 else ''} {joined_names} missing from borgmatic's configuration"
|
||||
)
|
||||
|
||||
|
||||
@@ -259,7 +265,7 @@ def run_restore(
|
||||
Run the "restore" action for the given repository, but only if the repository matches the
|
||||
requested repository in restore arguments.
|
||||
|
||||
Raise ValueError if a configured database could not be found to restore.
|
||||
Raise ValueError if a configured data source could not be found to restore.
|
||||
'''
|
||||
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||
repository, restore_arguments.repository
|
||||
@@ -267,14 +273,14 @@ def run_restore(
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f'{repository.get("label", repository["path"])}: Restoring databases from archive {restore_arguments.archive}'
|
||||
f'{repository.get("label", repository["path"])}: Restoring data sources from archive {restore_arguments.archive}'
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
@@ -287,7 +293,7 @@ def run_restore(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
archive_database_names = collect_archive_database_names(
|
||||
archive_data_source_names = collect_archive_data_source_names(
|
||||
repository['path'],
|
||||
archive_name,
|
||||
config,
|
||||
@@ -296,7 +302,9 @@ def run_restore(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
restore_names = find_databases_to_restore(restore_arguments.databases, archive_database_names)
|
||||
restore_names = find_data_sources_to_restore(
|
||||
restore_arguments.data_sources, archive_data_source_names
|
||||
)
|
||||
found_names = set()
|
||||
remaining_restore_names = {}
|
||||
connection_params = {
|
||||
@@ -307,20 +315,20 @@ def run_restore(
|
||||
'restore_path': restore_arguments.restore_path,
|
||||
}
|
||||
|
||||
for hook_name, database_names in restore_names.items():
|
||||
for database_name in database_names:
|
||||
found_hook_name, found_database = get_configured_database(
|
||||
config, archive_database_names, hook_name, database_name
|
||||
for hook_name, data_source_names in restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name
|
||||
)
|
||||
|
||||
if not found_database:
|
||||
if not found_data_source:
|
||||
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||
database_name
|
||||
data_source_name
|
||||
)
|
||||
continue
|
||||
|
||||
found_names.add(database_name)
|
||||
restore_single_database(
|
||||
found_names.add(data_source_name)
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
@@ -329,26 +337,26 @@ def run_restore(
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(found_database, **{'schemas': restore_arguments.schemas}),
|
||||
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
)
|
||||
|
||||
# For any database that weren't found via exact matches in the configuration, try to fallback
|
||||
# to "all" entries.
|
||||
for hook_name, database_names in remaining_restore_names.items():
|
||||
for database_name in database_names:
|
||||
found_hook_name, found_database = get_configured_database(
|
||||
config, archive_database_names, hook_name, database_name, 'all'
|
||||
# For any data sources that weren't found via exact matches in the configuration, try to
|
||||
# fallback to "all" entries.
|
||||
for hook_name, data_source_names in remaining_restore_names.items():
|
||||
for data_source_name in data_source_names:
|
||||
found_hook_name, found_data_source = get_configured_data_source(
|
||||
config, archive_data_source_names, hook_name, data_source_name, 'all'
|
||||
)
|
||||
|
||||
if not found_database:
|
||||
if not found_data_source:
|
||||
continue
|
||||
|
||||
found_names.add(database_name)
|
||||
database = copy.copy(found_database)
|
||||
database['name'] = database_name
|
||||
found_names.add(data_source_name)
|
||||
data_source = copy.copy(found_data_source)
|
||||
data_source['name'] = data_source_name
|
||||
|
||||
restore_single_database(
|
||||
restore_single_data_source(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
@@ -357,16 +365,16 @@ def run_restore(
|
||||
remote_path,
|
||||
archive_name,
|
||||
found_hook_name or hook_name,
|
||||
dict(database, **{'schemas': restore_arguments.schemas}),
|
||||
dict(data_source, **{'schemas': restore_arguments.schemas}),
|
||||
connection_params,
|
||||
)
|
||||
|
||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||
'remove_database_dumps',
|
||||
'remove_data_source_dumps',
|
||||
config,
|
||||
repository['path'],
|
||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
||||
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||
global_arguments.dry_run,
|
||||
)
|
||||
|
||||
ensure_databases_found(restore_names, remaining_restore_names, found_names)
|
||||
ensure_data_sources_found(restore_names, remaining_restore_names, found_names)
|
||||
|
||||
+25
-19
@@ -39,7 +39,11 @@ def parse_checks(config, only_checks=None):
|
||||
check_config['name'] for check_config in (config.get('checks', None) or DEFAULT_CHECKS)
|
||||
)
|
||||
checks = tuple(check.lower() for check in checks)
|
||||
|
||||
if 'disabled' in checks:
|
||||
logger.warning(
|
||||
'The "disabled" value for the "checks" option is deprecated and will be removed from a future release; use "skip_actions" instead'
|
||||
)
|
||||
if len(checks) > 1:
|
||||
logger.warning(
|
||||
'Multiple checks are configured, but one of them is "disabled"; not running any checks'
|
||||
@@ -119,6 +123,9 @@ def filter_checks_on_frequency(
|
||||
|
||||
Raise ValueError if a frequency cannot be parsed.
|
||||
'''
|
||||
if not checks:
|
||||
return checks
|
||||
|
||||
filtered_checks = list(checks)
|
||||
|
||||
if force:
|
||||
@@ -142,18 +149,20 @@ def filter_checks_on_frequency(
|
||||
if datetime.datetime.now() < check_time + frequency_delta:
|
||||
remaining = check_time + frequency_delta - datetime.datetime.now()
|
||||
logger.info(
|
||||
f'Skipping {check} check due to configured frequency; {remaining} until next check'
|
||||
f'Skipping {check} check due to configured frequency; {remaining} until next check (use --force to check anyway)'
|
||||
)
|
||||
filtered_checks.remove(check)
|
||||
|
||||
return tuple(filtered_checks)
|
||||
|
||||
|
||||
def make_archive_filter_flags(local_borg_version, config, checks, check_last=None, prefix=None):
|
||||
def make_archive_filter_flags(
|
||||
local_borg_version, config, checks, check_arguments, check_last=None, prefix=None
|
||||
):
|
||||
'''
|
||||
Given the local Borg version, a configuration dict, a parsed sequence of checks, the check last
|
||||
value, and a consistency check prefix, transform the checks into tuple of command-line flags for
|
||||
filtering archives in a check command.
|
||||
Given the local Borg version, a configuration dict, a parsed sequence of checks, check arguments
|
||||
as an argparse.Namespace instance, the check last value, and a consistency check prefix,
|
||||
transform the checks into tuple of command-line flags for filtering archives in a check command.
|
||||
|
||||
If a check_last value is given and "archives" is in checks, then include a "--last" flag. And if
|
||||
a prefix value is given and "archives" is in checks, then include a "--match-archives" flag.
|
||||
@@ -168,7 +177,7 @@ def make_archive_filter_flags(local_borg_version, config, checks, check_last=Non
|
||||
if prefix
|
||||
else (
|
||||
flags.make_match_archives_flags(
|
||||
config.get('match_archives'),
|
||||
check_arguments.match_archives or config.get('match_archives'),
|
||||
config.get('archive_name_format'),
|
||||
local_borg_version,
|
||||
)
|
||||
@@ -353,18 +362,15 @@ def check_archives(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
check_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
progress=None,
|
||||
repair=None,
|
||||
only_checks=None,
|
||||
force=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, local/remote commands to run,
|
||||
whether to include progress information, whether to attempt a repair, and an optional list of
|
||||
checks to use instead of configured checks, check the contained Borg archives for consistency.
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, check
|
||||
arguments as an argparse.Namespace instance, global arguments, and local/remote commands to run,
|
||||
check the contained Borg archives for consistency.
|
||||
|
||||
If there are no consistency checks to run, skip running them.
|
||||
|
||||
@@ -389,11 +395,11 @@ def check_archives(
|
||||
|
||||
check_last = config.get('check_last', None)
|
||||
prefix = config.get('prefix')
|
||||
configured_checks = parse_checks(config, only_checks)
|
||||
configured_checks = parse_checks(config, check_arguments.only_checks)
|
||||
lock_wait = None
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('check', '')
|
||||
archive_filter_flags = make_archive_filter_flags(
|
||||
local_borg_version, config, configured_checks, check_last, prefix
|
||||
local_borg_version, config, configured_checks, check_arguments, check_last, prefix
|
||||
)
|
||||
archives_check_id = make_archives_check_id(archive_filter_flags)
|
||||
|
||||
@@ -401,7 +407,7 @@ def check_archives(
|
||||
config,
|
||||
borg_repository_id,
|
||||
configured_checks,
|
||||
force,
|
||||
check_arguments.force,
|
||||
archives_check_id,
|
||||
)
|
||||
|
||||
@@ -416,13 +422,13 @@ def check_archives(
|
||||
|
||||
full_command = (
|
||||
(local_path, 'check')
|
||||
+ (('--repair',) if repair else ())
|
||||
+ (('--repair',) if check_arguments.repair else ())
|
||||
+ make_check_flags(checks, archive_filter_flags)
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ verbosity_flags
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (('--progress',) if check_arguments.progress else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
@@ -431,7 +437,7 @@ def check_archives(
|
||||
|
||||
# The Borg repair option triggers an interactive prompt, which won't work when output is
|
||||
# captured. And progress messes with the terminal directly.
|
||||
if repair or progress:
|
||||
if check_arguments.repair or check_arguments.progress:
|
||||
execute_command(
|
||||
full_command, output_file=DO_NOT_CAPTURE, extra_environment=borg_environment
|
||||
)
|
||||
|
||||
+17
-16
@@ -215,9 +215,6 @@ def make_list_filter_flags(local_borg_version, dry_run):
|
||||
return f'{base_flags}-'
|
||||
|
||||
|
||||
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
|
||||
|
||||
|
||||
def collect_borgmatic_source_directories(borgmatic_source_directory):
|
||||
'''
|
||||
Return a list of borgmatic-specific source directories used for state like database backups.
|
||||
@@ -354,7 +351,11 @@ def create_archive(
|
||||
expand_directories(
|
||||
tuple(config.get('source_directories', ()))
|
||||
+ borgmatic_source_directories
|
||||
+ tuple(global_arguments.used_config_paths)
|
||||
+ tuple(
|
||||
global_arguments.used_config_paths
|
||||
if config.get('store_config_files', True)
|
||||
else ()
|
||||
)
|
||||
)
|
||||
),
|
||||
additional_directory_devices=map_directories_to_devices(
|
||||
@@ -384,7 +385,7 @@ def create_archive(
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
|
||||
files_cache = config.get('files_cache')
|
||||
archive_name_format = config.get('archive_name_format', DEFAULT_ARCHIVE_NAME_FORMAT)
|
||||
archive_name_format = config.get('archive_name_format', flags.DEFAULT_ARCHIVE_NAME_FORMAT)
|
||||
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
|
||||
|
||||
if feature.available(feature.Feature.ATIME, local_borg_version):
|
||||
@@ -416,7 +417,7 @@ def create_archive(
|
||||
f'{repository_path}: Ignoring configured "read_special" value of false, as true is needed for database hooks.'
|
||||
)
|
||||
|
||||
create_command = (
|
||||
create_flags = (
|
||||
tuple(local_path.split(' '))
|
||||
+ ('create',)
|
||||
+ make_pattern_flags(config, pattern_file.name if pattern_file else None)
|
||||
@@ -445,12 +446,12 @@ def create_archive(
|
||||
)
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
|
||||
+ flags.make_repository_archive_flags(
|
||||
repository_path, archive_name_format, local_borg_version
|
||||
)
|
||||
+ (sources if not pattern_file else ())
|
||||
)
|
||||
|
||||
create_positional_arguments = flags.make_repository_archive_flags(
|
||||
repository_path, archive_name_format, local_borg_version
|
||||
) + (sources if not pattern_file else ())
|
||||
|
||||
if json:
|
||||
output_log_level = None
|
||||
elif list_files or (stats and not dry_run):
|
||||
@@ -469,7 +470,7 @@ def create_archive(
|
||||
if stream_processes and not config.get('read_special'):
|
||||
logger.debug(f'{repository_path}: Collecting special file paths')
|
||||
special_file_paths = collect_special_file_paths(
|
||||
create_command,
|
||||
create_flags + create_positional_arguments,
|
||||
local_path,
|
||||
working_directory,
|
||||
borg_environment,
|
||||
@@ -486,9 +487,9 @@ def create_archive(
|
||||
),
|
||||
pattern_file=exclude_file,
|
||||
)
|
||||
create_command += make_exclude_flags(config, exclude_file.name)
|
||||
create_flags += make_exclude_flags(config, exclude_file.name)
|
||||
|
||||
create_command += (
|
||||
create_flags += (
|
||||
(('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
|
||||
+ (('--stats',) if stats and not json and not dry_run else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) and not json else ())
|
||||
@@ -498,7 +499,7 @@ def create_archive(
|
||||
|
||||
if stream_processes:
|
||||
return execute_command_with_processes(
|
||||
create_command,
|
||||
create_flags + create_positional_arguments,
|
||||
stream_processes,
|
||||
output_log_level,
|
||||
output_file,
|
||||
@@ -508,14 +509,14 @@ def create_archive(
|
||||
)
|
||||
elif output_log_level is None:
|
||||
return execute_command_and_capture_output(
|
||||
create_command,
|
||||
create_flags + create_positional_arguments,
|
||||
working_directory=working_directory,
|
||||
extra_environment=borg_environment,
|
||||
borg_local_path=local_path,
|
||||
)
|
||||
else:
|
||||
execute_command(
|
||||
create_command,
|
||||
create_flags + create_positional_arguments,
|
||||
output_log_level,
|
||||
output_file,
|
||||
borg_local_path=local_path,
|
||||
|
||||
@@ -38,14 +38,16 @@ def make_environment(config):
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_DOWNCASE_ENVIRONMENT_VARIABLE.items():
|
||||
value = config.get(option_name, False)
|
||||
environment[environment_variable_name] = 'yes' if value else 'no'
|
||||
value = config.get(option_name)
|
||||
if value is not None:
|
||||
environment[environment_variable_name] = 'yes' if value else 'no'
|
||||
|
||||
for (
|
||||
option_name,
|
||||
environment_variable_name,
|
||||
) in DEFAULT_BOOL_OPTION_TO_UPPERCASE_ENVIRONMENT_VARIABLE.items():
|
||||
value = config.get(option_name, False)
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
value = config.get(option_name)
|
||||
if value is not None:
|
||||
environment[environment_variable_name] = 'YES' if value else 'NO'
|
||||
|
||||
return environment
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import environment, flags
|
||||
from borgmatic.execute import DO_NOT_CAPTURE, execute_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def export_key(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
export_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, and
|
||||
optional local and remote Borg paths, export the repository key to the destination path
|
||||
indicated in the export arguments.
|
||||
|
||||
If the destination path is empty or "-", then print the key to stdout instead of to a file.
|
||||
|
||||
Raise FileExistsError if a path is given but it already exists on disk.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
umask = config.get('umask', None)
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
|
||||
if export_arguments.path and export_arguments.path != '-':
|
||||
if os.path.exists(export_arguments.path):
|
||||
raise FileExistsError(
|
||||
f'Destination path {export_arguments.path} already exists. Aborting.'
|
||||
)
|
||||
|
||||
output_file = None
|
||||
else:
|
||||
output_file = DO_NOT_CAPTURE
|
||||
|
||||
full_command = (
|
||||
(local_path, 'key', 'export')
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--log-json',) if global_arguments.log_json else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ flags.make_flags('paper', export_arguments.paper)
|
||||
+ flags.make_flags('qr-html', export_arguments.qr_html)
|
||||
+ flags.make_repository_flags(
|
||||
repository_path,
|
||||
local_borg_version,
|
||||
)
|
||||
+ ((export_arguments.path,) if output_file is None else ())
|
||||
)
|
||||
|
||||
if global_arguments.dry_run:
|
||||
logging.info(f'{repository_path}: Skipping key export (dry run)')
|
||||
return
|
||||
|
||||
execute_command(
|
||||
full_command,
|
||||
output_file=output_file,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=environment.make_environment(config),
|
||||
)
|
||||
+38
-6
@@ -1,8 +1,12 @@
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from borgmatic.borg import feature
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_flags(name, value):
|
||||
'''
|
||||
@@ -59,12 +63,15 @@ def make_repository_archive_flags(repository_path, archive, local_borg_version):
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' # noqa: FS003
|
||||
|
||||
|
||||
def make_match_archives_flags(match_archives, archive_name_format, local_borg_version):
|
||||
'''
|
||||
Return match archives flags based on the given match archives value, if any. If it isn't set,
|
||||
return match archives flags to match archives created with the given archive name format, if
|
||||
any. This is done by replacing certain archive name format placeholders for ephemeral data (like
|
||||
"{now}") with globs.
|
||||
return match archives flags to match archives created with the given (or default) archive name
|
||||
format. This is done by replacing certain archive name format placeholders for ephemeral data
|
||||
(like "{now}") with globs.
|
||||
'''
|
||||
if match_archives:
|
||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
||||
@@ -72,12 +79,37 @@ def make_match_archives_flags(match_archives, archive_name_format, local_borg_ve
|
||||
else:
|
||||
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
|
||||
|
||||
if not archive_name_format:
|
||||
return ()
|
||||
derived_match_archives = re.sub(
|
||||
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or DEFAULT_ARCHIVE_NAME_FORMAT
|
||||
)
|
||||
|
||||
derived_match_archives = re.sub(r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format)
|
||||
if derived_match_archives == '*':
|
||||
return ()
|
||||
|
||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
||||
return ('--match-archives', f'sh:{derived_match_archives}')
|
||||
else:
|
||||
return ('--glob-archives', f'{derived_match_archives}')
|
||||
|
||||
|
||||
def warn_for_aggressive_archive_flags(json_command, json_output):
|
||||
'''
|
||||
Given a JSON archives command and the resulting JSON string output from running it, parse the
|
||||
JSON and warn if the command used an archive flag but the output indicates zero archives were
|
||||
found.
|
||||
'''
|
||||
archive_flags_used = {'--glob-archives', '--match-archives'}.intersection(set(json_command))
|
||||
|
||||
if not archive_flags_used:
|
||||
return
|
||||
|
||||
try:
|
||||
if len(json.loads(json_output)['archives']) == 0:
|
||||
logger.warning('An archive filter was applied, but no matching archives were found.')
|
||||
logger.warning(
|
||||
'Try adding --match-archives "*" or adjusting archive_name_format/match_archives in configuration.'
|
||||
)
|
||||
except json.JSONDecodeError as error:
|
||||
logger.debug(f'Cannot parse JSON output from archive command: {error}')
|
||||
except (TypeError, KeyError):
|
||||
logger.debug('Cannot parse JSON output from archive command: No "archives" key found')
|
||||
|
||||
+61
-23
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import borgmatic.logger
|
||||
@@ -7,24 +8,21 @@ from borgmatic.execute import execute_command, execute_command_and_capture_outpu
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def display_archives_info(
|
||||
def make_info_command(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
info_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
local_path,
|
||||
remote_path,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, global
|
||||
arguments as an argparse.Namespace, and the arguments to the info action, display summary
|
||||
information for Borg archives in the repository or return JSON summary information.
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, the
|
||||
arguments to the info action as an argparse.Namespace, and global arguments, return a command
|
||||
as a tuple to display summary information for archives in the repository.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
lock_wait = config.get('lock_wait', None)
|
||||
|
||||
full_command = (
|
||||
return (
|
||||
(local_path, 'info')
|
||||
+ (
|
||||
('--info',)
|
||||
@@ -38,7 +36,7 @@ def display_archives_info(
|
||||
)
|
||||
+ flags.make_flags('remote-path', remote_path)
|
||||
+ flags.make_flags('log-json', global_arguments.log_json)
|
||||
+ flags.make_flags('lock-wait', lock_wait)
|
||||
+ flags.make_flags('lock-wait', config.get('lock_wait'))
|
||||
+ (
|
||||
(
|
||||
flags.make_flags('match-archives', f'sh:{info_arguments.prefix}*')
|
||||
@@ -62,16 +60,56 @@ def display_archives_info(
|
||||
+ flags.make_repository_flags(repository_path, local_borg_version)
|
||||
)
|
||||
|
||||
|
||||
def display_archives_info(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
info_arguments,
|
||||
global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, a configuration dict, the local Borg version, the
|
||||
arguments to the info action as an argparse.Namespace, and global arguments, display summary
|
||||
information for Borg archives in the repository or return JSON summary information.
|
||||
'''
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
main_command = make_info_command(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
info_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
json_command = make_info_command(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(**dict(info_arguments.__dict__, json=True)),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
json_info = execute_command_and_capture_output(
|
||||
json_command,
|
||||
extra_environment=environment.make_environment(config),
|
||||
borg_local_path=local_path,
|
||||
)
|
||||
|
||||
if info_arguments.json:
|
||||
return execute_command_and_capture_output(
|
||||
full_command,
|
||||
extra_environment=environment.make_environment(config),
|
||||
borg_local_path=local_path,
|
||||
)
|
||||
else:
|
||||
execute_command(
|
||||
full_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=environment.make_environment(config),
|
||||
)
|
||||
return json_info
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_info)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=environment.make_environment(config),
|
||||
)
|
||||
|
||||
@@ -92,13 +92,13 @@ def capture_archive_listing(
|
||||
config,
|
||||
local_borg_version,
|
||||
global_arguments,
|
||||
list_path=None,
|
||||
list_paths=None,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
):
|
||||
'''
|
||||
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
|
||||
version, global arguments as an argparse.Namespace, the archive path in which to list files, and
|
||||
version, global arguments as an argparse.Namespace, the archive paths in which to list files, and
|
||||
local and remote Borg paths, capture the output of listing that archive and return it as a list
|
||||
of file paths.
|
||||
'''
|
||||
@@ -113,7 +113,7 @@ def capture_archive_listing(
|
||||
argparse.Namespace(
|
||||
repository=repository_path,
|
||||
archive=archive,
|
||||
paths=[f'sh:{list_path}'],
|
||||
paths=[f'sh:{path}' for path in list_paths] if list_paths else None,
|
||||
find_paths=None,
|
||||
json=None,
|
||||
format='{path}{NL}', # noqa: FS003
|
||||
|
||||
@@ -26,7 +26,7 @@ def make_prune_flags(config, local_borg_version):
|
||||
flag_pairs = (
|
||||
('--' + option_name.replace('_', '-'), str(value))
|
||||
for option_name, value in config.items()
|
||||
if option_name.startswith('keep_')
|
||||
if option_name.startswith('keep_') and option_name != 'keep_exclude_tags'
|
||||
)
|
||||
prefix = config.get('prefix')
|
||||
|
||||
|
||||
+24
-10
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import borgmatic.logger
|
||||
@@ -137,15 +138,28 @@ def list_repository(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
json_command = make_rlist_command(
|
||||
repository_path,
|
||||
config,
|
||||
local_borg_version,
|
||||
argparse.Namespace(**dict(rlist_arguments.__dict__, json=True)),
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
|
||||
json_listing = execute_command_and_capture_output(
|
||||
json_command, extra_environment=borg_environment, borg_local_path=local_path
|
||||
)
|
||||
|
||||
if rlist_arguments.json:
|
||||
return execute_command_and_capture_output(
|
||||
main_command, extra_environment=borg_environment, borg_local_path=local_path
|
||||
)
|
||||
else:
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=borg_environment,
|
||||
)
|
||||
return json_listing
|
||||
|
||||
flags.warn_for_aggressive_archive_flags(json_command, json_listing)
|
||||
|
||||
execute_command(
|
||||
main_command,
|
||||
output_log_level=logging.ANSWER,
|
||||
borg_local_path=local_path,
|
||||
extra_environment=borg_environment,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ ACTION_ALIASES = {
|
||||
'info': ['-i'],
|
||||
'transfer': [],
|
||||
'break-lock': [],
|
||||
'key': [],
|
||||
'borg': [],
|
||||
}
|
||||
|
||||
@@ -258,28 +259,28 @@ def make_parsers():
|
||||
type=int,
|
||||
choices=range(-2, 3),
|
||||
default=0,
|
||||
help='Display verbose progress to the console (disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2)',
|
||||
help='Display verbose progress to the console: -2 (disabled), -1 (errors only), 0 (responses to actions, the default), 1 (info about steps borgmatic is taking), or 2 (debug)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--syslog-verbosity',
|
||||
type=int,
|
||||
choices=range(-2, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to syslog (disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2). Ignored when console is interactive or --log-file is given',
|
||||
default=-2,
|
||||
help='Log verbose progress to syslog: -2 (disabled, the default), -1 (errors only), 0 (responses to actions), 1 (info about steps borgmatic is taking), or 2 (debug)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file-verbosity',
|
||||
type=int,
|
||||
choices=range(-2, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to log file (disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2). Only used when --log-file is given',
|
||||
default=1,
|
||||
help='When --log-file is given, log verbose progress to file: -2 (disabled), -1 (errors only), 0 (responses to actions), 1 (info about steps borgmatic is taking, the default), or 2 (debug)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--monitoring-verbosity',
|
||||
type=int,
|
||||
choices=range(-2, 3),
|
||||
default=0,
|
||||
help='Log verbose progress to monitoring integrations that support logging (from disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2)',
|
||||
default=1,
|
||||
help='When a monitoring integration supporting logging is configured, log verbose progress to it: -2 (disabled), -1 (errors only), responses to actions (0), 1 (info about steps borgmatic is taking, the default), or 2 (debug)',
|
||||
)
|
||||
global_group.add_argument(
|
||||
'--log-file',
|
||||
@@ -331,9 +332,8 @@ def make_parsers():
|
||||
|
||||
global_plus_action_parser = ArgumentParser(
|
||||
description='''
|
||||
Simple, configuration-driven backup software for servers and workstations. If none of
|
||||
the action options are given, then borgmatic defaults to: create, prune, compact, and
|
||||
check.
|
||||
Simple, configuration-driven backup software for servers and workstations. If no actions
|
||||
are given, then borgmatic defaults to: create, prune, compact, and check.
|
||||
''',
|
||||
parents=[global_parser],
|
||||
)
|
||||
@@ -604,11 +604,18 @@ def make_parsers():
|
||||
action='store_true',
|
||||
help='Attempt to repair any inconsistencies found (for interactive use)',
|
||||
)
|
||||
check_group.add_argument(
|
||||
'-a',
|
||||
'--match-archives',
|
||||
'--glob-archives',
|
||||
metavar='PATTERN',
|
||||
help='Only check archives with names matching this pattern',
|
||||
)
|
||||
check_group.add_argument(
|
||||
'--only',
|
||||
metavar='CHECK',
|
||||
choices=('repository', 'archives', 'data', 'extract'),
|
||||
dest='only',
|
||||
dest='only_checks',
|
||||
action='append',
|
||||
help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks (subject to configured frequency, can specify flag multiple times)',
|
||||
)
|
||||
@@ -906,8 +913,8 @@ def make_parsers():
|
||||
restore_parser = action_parsers.add_parser(
|
||||
'restore',
|
||||
aliases=ACTION_ALIASES['restore'],
|
||||
help='Restore database dumps from a named archive',
|
||||
description='Restore database dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
|
||||
help='Restore data source (e.g. database) dumps from a named archive',
|
||||
description='Restore data source (e.g. database) dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
|
||||
add_help=False,
|
||||
)
|
||||
restore_group = restore_parser.add_argument_group('restore arguments')
|
||||
@@ -919,18 +926,19 @@ def make_parsers():
|
||||
'--archive', help='Name of archive to restore from (or "latest")', required=True
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--data-source',
|
||||
'--database',
|
||||
metavar='NAME',
|
||||
dest='databases',
|
||||
dest='data_sources',
|
||||
action='append',
|
||||
help="Name of database to restore from archive, must be defined in borgmatic's configuration, can specify flag multiple times, defaults to all databases",
|
||||
help="Name of data source (e.g. database) to restore from archive, must be defined in borgmatic's configuration, can specify flag multiple times, defaults to all data sources in the archive",
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--schema',
|
||||
metavar='NAME',
|
||||
dest='schemas',
|
||||
action='append',
|
||||
help='Name of schema to restore from the database, can specify flag multiple times, defaults to all schemas. Schemas are only supported for PostgreSQL and MongoDB databases',
|
||||
help='Name of schema to restore from the data source, can specify flag multiple times, defaults to all schemas. Schemas are only supported for PostgreSQL and MongoDB databases',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--hostname',
|
||||
@@ -938,7 +946,7 @@ def make_parsers():
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--port',
|
||||
help='Port to restore to. Defaults to the "restore_port" option in borgmatic\'s configuration',
|
||||
help='Database port to restore to. Defaults to the "restore_port" option in borgmatic\'s configuration',
|
||||
)
|
||||
restore_group.add_argument(
|
||||
'--username',
|
||||
@@ -1177,6 +1185,51 @@ def make_parsers():
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
key_parser = action_parsers.add_parser(
|
||||
'key',
|
||||
aliases=ACTION_ALIASES['key'],
|
||||
help='Perform repository key related operations',
|
||||
description='Perform repository key related operations',
|
||||
add_help=False,
|
||||
)
|
||||
|
||||
key_group = key_parser.add_argument_group('key arguments')
|
||||
key_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
|
||||
|
||||
key_parsers = key_parser.add_subparsers(
|
||||
title='key sub-actions',
|
||||
)
|
||||
|
||||
key_export_parser = key_parsers.add_parser(
|
||||
'export',
|
||||
help='Export a copy of the repository key for safekeeping in case the original goes missing or gets damaged',
|
||||
description='Export a copy of the repository key for safekeeping in case the original goes missing or gets damaged',
|
||||
add_help=False,
|
||||
)
|
||||
key_export_group = key_export_parser.add_argument_group('key export arguments')
|
||||
key_export_group.add_argument(
|
||||
'--paper',
|
||||
action='store_true',
|
||||
help='Export the key in a text format suitable for printing and later manual entry',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--qr-html',
|
||||
action='store_true',
|
||||
help='Export the key in an HTML format suitable for printing and later manual entry or QR code scanning',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--repository',
|
||||
help='Path of repository to export the key for, defaults to the configured repository if there is only one',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'--path',
|
||||
metavar='PATH',
|
||||
help='Path to export the key to, defaults to stdout (but be careful about dirtying the output with --verbosity)',
|
||||
)
|
||||
key_export_group.add_argument(
|
||||
'-h', '--help', action='help', help='Show this help message and exit'
|
||||
)
|
||||
|
||||
borg_parser = action_parsers.add_parser(
|
||||
'borg',
|
||||
aliases=ACTION_ALIASES['borg'],
|
||||
@@ -1282,4 +1335,7 @@ def parse_arguments(*unparsed_arguments):
|
||||
'With the info action, only one of --archive, --prefix, or --match-archives flags can be used.'
|
||||
)
|
||||
|
||||
if 'borg' in arguments and arguments['global'].dry_run:
|
||||
raise ValueError('With the borg action, --dry-run is not supported.')
|
||||
|
||||
return arguments
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections
|
||||
import importlib.metadata
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -9,11 +10,6 @@ from subprocess import CalledProcessError
|
||||
|
||||
import colorama
|
||||
|
||||
try:
|
||||
import importlib_metadata
|
||||
except ModuleNotFoundError: # pragma: nocover
|
||||
import importlib.metadata as importlib_metadata
|
||||
|
||||
import borgmatic.actions.borg
|
||||
import borgmatic.actions.break_lock
|
||||
import borgmatic.actions.check
|
||||
@@ -22,6 +18,7 @@ import borgmatic.actions.config.bootstrap
|
||||
import borgmatic.actions.config.generate
|
||||
import borgmatic.actions.config.validate
|
||||
import borgmatic.actions.create
|
||||
import borgmatic.actions.export_key
|
||||
import borgmatic.actions.export_tar
|
||||
import borgmatic.actions.extract
|
||||
import borgmatic.actions.info
|
||||
@@ -69,6 +66,12 @@ def run_configuration(config_filename, config, arguments):
|
||||
using_primary_action = {'create', 'prune', 'compact', 'check'}.intersection(arguments)
|
||||
monitoring_log_level = verbosity_to_log_level(global_arguments.monitoring_verbosity)
|
||||
monitoring_hooks_are_activated = using_primary_action and monitoring_log_level != DISABLED
|
||||
skip_actions = config.get('skip_actions')
|
||||
|
||||
if skip_actions:
|
||||
logger.debug(
|
||||
f"{config_filename}: Skipping {'/'.join(skip_actions)} action{'s' if len(skip_actions) > 1 else ''} due to configured skip_actions"
|
||||
)
|
||||
|
||||
try:
|
||||
local_borg_version = borg_version.local_borg_version(config, local_path)
|
||||
@@ -273,6 +276,7 @@ def run_actions(
|
||||
'repositories': ','.join([repo['path'] for repo in config['repositories']]),
|
||||
'log_file': global_arguments.log_file if global_arguments.log_file else '',
|
||||
}
|
||||
skip_actions = set(config.get('skip_actions', {}))
|
||||
|
||||
command.execute_hook(
|
||||
config.get('before_actions'),
|
||||
@@ -284,7 +288,7 @@ def run_actions(
|
||||
)
|
||||
|
||||
for action_name, action_arguments in arguments.items():
|
||||
if action_name == 'rcreate':
|
||||
if action_name == 'rcreate' and action_name not in skip_actions:
|
||||
borgmatic.actions.rcreate.run_rcreate(
|
||||
repository,
|
||||
config,
|
||||
@@ -294,7 +298,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'transfer':
|
||||
elif action_name == 'transfer' and action_name not in skip_actions:
|
||||
borgmatic.actions.transfer.run_transfer(
|
||||
repository,
|
||||
config,
|
||||
@@ -304,7 +308,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'create':
|
||||
elif action_name == 'create' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.create.run_create(
|
||||
config_filename,
|
||||
repository,
|
||||
@@ -317,7 +321,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'prune':
|
||||
elif action_name == 'prune' and action_name not in skip_actions:
|
||||
borgmatic.actions.prune.run_prune(
|
||||
config_filename,
|
||||
repository,
|
||||
@@ -330,7 +334,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'compact':
|
||||
elif action_name == 'compact' and action_name not in skip_actions:
|
||||
borgmatic.actions.compact.run_compact(
|
||||
config_filename,
|
||||
repository,
|
||||
@@ -343,7 +347,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'check':
|
||||
elif action_name == 'check' and action_name not in skip_actions:
|
||||
if checks.repository_enabled_for_checks(repository, config):
|
||||
borgmatic.actions.check.run_check(
|
||||
config_filename,
|
||||
@@ -356,7 +360,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'extract':
|
||||
elif action_name == 'extract' and action_name not in skip_actions:
|
||||
borgmatic.actions.extract.run_extract(
|
||||
config_filename,
|
||||
repository,
|
||||
@@ -368,7 +372,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export-tar':
|
||||
elif action_name == 'export-tar' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_tar.run_export_tar(
|
||||
repository,
|
||||
config,
|
||||
@@ -378,7 +382,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'mount':
|
||||
elif action_name == 'mount' and action_name not in skip_actions:
|
||||
borgmatic.actions.mount.run_mount(
|
||||
repository,
|
||||
config,
|
||||
@@ -388,7 +392,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'restore':
|
||||
elif action_name == 'restore' and action_name not in skip_actions:
|
||||
borgmatic.actions.restore.run_restore(
|
||||
repository,
|
||||
config,
|
||||
@@ -398,7 +402,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'rlist':
|
||||
elif action_name == 'rlist' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.rlist.run_rlist(
|
||||
repository,
|
||||
config,
|
||||
@@ -408,7 +412,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'list':
|
||||
elif action_name == 'list' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.list.run_list(
|
||||
repository,
|
||||
config,
|
||||
@@ -418,7 +422,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'rinfo':
|
||||
elif action_name == 'rinfo' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.rinfo.run_rinfo(
|
||||
repository,
|
||||
config,
|
||||
@@ -428,7 +432,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'info':
|
||||
elif action_name == 'info' and action_name not in skip_actions:
|
||||
yield from borgmatic.actions.info.run_info(
|
||||
repository,
|
||||
config,
|
||||
@@ -438,7 +442,7 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'break-lock':
|
||||
elif action_name == 'break-lock' and action_name not in skip_actions:
|
||||
borgmatic.actions.break_lock.run_break_lock(
|
||||
repository,
|
||||
config,
|
||||
@@ -448,7 +452,17 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg':
|
||||
elif action_name == 'export' and action_name not in skip_actions:
|
||||
borgmatic.actions.export_key.run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg' and action_name not in skip_actions:
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
config,
|
||||
@@ -544,9 +558,6 @@ def log_record(suppress_log=False, **kwargs):
|
||||
return record
|
||||
|
||||
|
||||
MAX_CAPTURED_OUTPUT_LENGTH = 1000
|
||||
|
||||
|
||||
def log_error_records(
|
||||
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
|
||||
):
|
||||
@@ -568,20 +579,24 @@ def log_error_records(
|
||||
raise error
|
||||
except CalledProcessError as error:
|
||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||
|
||||
if error.output:
|
||||
try:
|
||||
output = error.output.decode('utf-8')
|
||||
except (UnicodeDecodeError, AttributeError):
|
||||
output = error.output
|
||||
|
||||
# Suppress these logs for now and save full error output for the log summary at the end.
|
||||
yield log_record(
|
||||
levelno=levelno,
|
||||
levelname=level_name,
|
||||
msg=output[:MAX_CAPTURED_OUTPUT_LENGTH]
|
||||
+ ' ...' * (len(output) > MAX_CAPTURED_OUTPUT_LENGTH),
|
||||
suppress_log=True,
|
||||
)
|
||||
# Suppress these logs for now and save the error output for the log summary at the end.
|
||||
# Log a separate record per line, as some errors can be really verbose and overflow the
|
||||
# per-record size limits imposed by some logging backends.
|
||||
for output_line in output.splitlines():
|
||||
yield log_record(
|
||||
levelno=levelno,
|
||||
levelname=level_name,
|
||||
msg=output_line,
|
||||
suppress_log=True,
|
||||
)
|
||||
|
||||
yield log_record(levelno=levelno, levelname=level_name, msg=error)
|
||||
except (ValueError, OSError) as error:
|
||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||
@@ -815,7 +830,7 @@ def main(extra_summary_logs=[]): # pragma: no cover
|
||||
|
||||
global_arguments = arguments['global']
|
||||
if global_arguments.version:
|
||||
print(importlib_metadata.version('borgmatic'))
|
||||
print(importlib.metadata.version('borgmatic'))
|
||||
sys.exit(0)
|
||||
if global_arguments.bash_completion:
|
||||
print(borgmatic.commands.completion.bash.bash_completion())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
def repository_enabled_for_checks(repository, consistency):
|
||||
def repository_enabled_for_checks(repository, config):
|
||||
'''
|
||||
Given a repository name and a consistency configuration dict, return whether the repository
|
||||
is enabled to have consistency checks run.
|
||||
Given a repository name and a configuration dict, return whether the
|
||||
repository is enabled to have consistency checks run.
|
||||
'''
|
||||
if not consistency.get('check_repositories'):
|
||||
if not config.get('check_repositories'):
|
||||
return True
|
||||
|
||||
return repository in consistency['check_repositories']
|
||||
return repository in config['check_repositories']
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
def coerce_scalar(value):
|
||||
'''
|
||||
Given a configuration value, coerce it to an integer or a boolean as appropriate and return the
|
||||
result.
|
||||
'''
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
if value == 'true' or value == 'True':
|
||||
return True
|
||||
if value == 'false' or value == 'False':
|
||||
return False
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def apply_constants(value, constants):
|
||||
'''
|
||||
Given a configuration value (bool, dict, int, list, or string) and a dict of named constants,
|
||||
replace any configuration string values of the form "{constant}" (or containing it) with the
|
||||
value of the correspondingly named key from the constants. Recurse as necessary into nested
|
||||
configuration to find values to replace.
|
||||
|
||||
For instance, if a configuration value contains "{foo}", replace it with the value of the "foo"
|
||||
key found within the configuration's "constants".
|
||||
|
||||
Return the configuration value and modify the original.
|
||||
'''
|
||||
if not value or not constants:
|
||||
return value
|
||||
|
||||
if isinstance(value, str):
|
||||
for constant_name, constant_value in constants.items():
|
||||
value = value.replace('{' + constant_name + '}', str(constant_value))
|
||||
|
||||
# Support constants within non-string scalars by coercing the value to its appropriate type.
|
||||
value = coerce_scalar(value)
|
||||
elif isinstance(value, list):
|
||||
for index, list_value in enumerate(value):
|
||||
value[index] = apply_constants(list_value, constants)
|
||||
elif isinstance(value, dict):
|
||||
for option_name, option_value in value.items():
|
||||
value[option_name] = apply_constants(option_value, constants)
|
||||
|
||||
return value
|
||||
@@ -1,21 +1,22 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
_VARIABLE_PATTERN = re.compile(
|
||||
VARIABLE_PATTERN = re.compile(
|
||||
r'(?P<escape>\\)?(?P<variable>\$\{(?P<name>[A-Za-z0-9_]+)((:?-)(?P<default>[^}]+))?\})'
|
||||
)
|
||||
|
||||
|
||||
def _resolve_string(matcher):
|
||||
def resolve_string(matcher):
|
||||
'''
|
||||
Get the value from environment given a matcher containing a name and an optional default value.
|
||||
If the variable is not defined in environment and no default value is provided, an Error is raised.
|
||||
Given a matcher containing a name and an optional default value, get the value from environment.
|
||||
|
||||
Raise ValueError if the variable is not defined in environment and no default value is provided.
|
||||
'''
|
||||
if matcher.group('escape') is not None:
|
||||
# in case of escaped envvar, unescape it
|
||||
# In the case of an escaped environment variable, unescape it.
|
||||
return matcher.group('variable')
|
||||
|
||||
# resolve the env var
|
||||
# Resolve the environment variable.
|
||||
name, default = matcher.group('name'), matcher.group('default')
|
||||
out = os.getenv(name, default=default)
|
||||
|
||||
@@ -27,19 +28,24 @@ def _resolve_string(matcher):
|
||||
|
||||
def resolve_env_variables(item):
|
||||
'''
|
||||
Resolves variables like or ${FOO} from given configuration with values from process environment
|
||||
Supported formats:
|
||||
- ${FOO} will return FOO env variable
|
||||
- ${FOO-bar} or ${FOO:-bar} will return FOO env variable if it exists, else "bar"
|
||||
Resolves variables like or ${FOO} from given configuration with values from process environment.
|
||||
|
||||
If any variable is missing in environment and no default value is provided, an Error is raised.
|
||||
Supported formats:
|
||||
|
||||
* ${FOO} will return FOO env variable
|
||||
* ${FOO-bar} or ${FOO:-bar} will return FOO env variable if it exists, else "bar"
|
||||
|
||||
Raise if any variable is missing in environment and no default value is provided.
|
||||
'''
|
||||
if isinstance(item, str):
|
||||
return _VARIABLE_PATTERN.sub(_resolve_string, item)
|
||||
return VARIABLE_PATTERN.sub(resolve_string, item)
|
||||
|
||||
if isinstance(item, list):
|
||||
for i, subitem in enumerate(item):
|
||||
item[i] = resolve_env_variables(subitem)
|
||||
for index, subitem in enumerate(item):
|
||||
item[index] = resolve_env_variables(subitem)
|
||||
|
||||
if isinstance(item, dict):
|
||||
for key, value in item.items():
|
||||
item[key] = resolve_env_variables(value)
|
||||
|
||||
return item
|
||||
|
||||
@@ -3,7 +3,7 @@ import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from ruamel import yaml
|
||||
import ruamel.yaml
|
||||
|
||||
from borgmatic.config import load, normalize
|
||||
|
||||
@@ -17,7 +17,7 @@ def insert_newline_before_comment(config, field_name):
|
||||
field and its comments.
|
||||
'''
|
||||
config.ca.items[field_name][1].insert(
|
||||
0, yaml.tokens.CommentToken('\n', yaml.error.CommentMark(0), None)
|
||||
0, ruamel.yaml.tokens.CommentToken('\n', ruamel.yaml.error.CommentMark(0), None)
|
||||
)
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ def schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
|
||||
return example
|
||||
|
||||
if schema_type == 'array':
|
||||
config = yaml.comments.CommentedSeq(
|
||||
config = ruamel.yaml.comments.CommentedSeq(
|
||||
[schema_to_sample_configuration(schema['items'], level, parent_is_sequence=True)]
|
||||
)
|
||||
add_comments_to_configuration_sequence(config, schema, indent=(level * INDENT))
|
||||
elif schema_type == 'object':
|
||||
config = yaml.comments.CommentedMap(
|
||||
config = ruamel.yaml.comments.CommentedMap(
|
||||
[
|
||||
(field_name, schema_to_sample_configuration(sub_schema, level + 1))
|
||||
for field_name, sub_schema in schema['properties'].items()
|
||||
@@ -101,7 +101,7 @@ def render_configuration(config):
|
||||
'''
|
||||
Given a config data structure of nested OrderedDicts, render the config as YAML and return it.
|
||||
'''
|
||||
dumper = yaml.YAML()
|
||||
dumper = ruamel.yaml.YAML(typ='rt')
|
||||
dumper.indent(mapping=INDENT, sequence=INDENT + SEQUENCE_INDENT, offset=INDENT)
|
||||
rendered = io.StringIO()
|
||||
dumper.dump(config, rendered)
|
||||
@@ -236,7 +236,9 @@ def merge_source_configuration_into_destination(destination_config, source_confi
|
||||
for field_name, source_value in source_config.items():
|
||||
# Since this key/value is from the source configuration, leave it uncommented and remove any
|
||||
# sentinel that would cause it to get commented out.
|
||||
remove_commented_out_sentinel(destination_config, field_name)
|
||||
remove_commented_out_sentinel(
|
||||
ruamel.yaml.comments.CommentedMap(destination_config), field_name
|
||||
)
|
||||
|
||||
# This is a mapping. Recurse for this key/value.
|
||||
if isinstance(source_value, collections.abc.Mapping):
|
||||
@@ -248,7 +250,7 @@ def merge_source_configuration_into_destination(destination_config, source_confi
|
||||
# This is a sequence. Recurse for each item in it.
|
||||
if isinstance(source_value, collections.abc.Sequence) and not isinstance(source_value, str):
|
||||
destination_value = destination_config[field_name]
|
||||
destination_config[field_name] = yaml.comments.CommentedSeq(
|
||||
destination_config[field_name] = ruamel.yaml.comments.CommentedSeq(
|
||||
[
|
||||
merge_source_configuration_into_destination(
|
||||
destination_value[index] if index < len(destination_value) else None,
|
||||
@@ -275,7 +277,7 @@ def generate_sample_configuration(
|
||||
schema. If a source filename is provided, merge the parsed contents of that configuration into
|
||||
the generated configuration.
|
||||
'''
|
||||
schema = yaml.round_trip_load(open(schema_filename))
|
||||
schema = ruamel.yaml.YAML(typ='safe').load(open(schema_filename))
|
||||
source_config = None
|
||||
|
||||
if source_filename:
|
||||
|
||||
+200
-123
@@ -1,6 +1,7 @@
|
||||
import functools
|
||||
import json
|
||||
import itertools
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
|
||||
import ruamel.yaml
|
||||
@@ -8,34 +9,67 @@ import ruamel.yaml
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def probe_and_include_file(filename, include_directories):
|
||||
'''
|
||||
Given a filename to include and a list of include directories to search for matching files,
|
||||
probe for the file, load it, and return the loaded configuration as a data structure of nested
|
||||
dicts, lists, etc.
|
||||
|
||||
Raise FileNotFoundError if the included file was not found.
|
||||
'''
|
||||
expanded_filename = os.path.expanduser(filename)
|
||||
|
||||
if os.path.isabs(expanded_filename):
|
||||
return load_configuration(expanded_filename)
|
||||
|
||||
candidate_filenames = {
|
||||
os.path.join(directory, expanded_filename) for directory in include_directories
|
||||
}
|
||||
|
||||
for candidate_filename in candidate_filenames:
|
||||
if os.path.exists(candidate_filename):
|
||||
return load_configuration(candidate_filename)
|
||||
|
||||
raise FileNotFoundError(
|
||||
f'Could not find include {filename} at {" or ".join(candidate_filenames)}'
|
||||
)
|
||||
|
||||
|
||||
def include_configuration(loader, filename_node, include_directory):
|
||||
'''
|
||||
Given a ruamel.yaml.loader.Loader, a ruamel.yaml.serializer.ScalarNode containing the included
|
||||
filename, and an include directory path to search for matching files, load the given YAML
|
||||
filename (ignoring the given loader so we can use our own) and return its contents as a data
|
||||
structure of nested dicts and lists. If the filename is relative, probe for it within 1. the
|
||||
current working directory and 2. the given include directory.
|
||||
Given a ruamel.yaml.loader.Loader, a ruamel.yaml.nodes.ScalarNode containing the included
|
||||
filename (or a list containing multiple such filenames), and an include directory path to search
|
||||
for matching files, load the given YAML filenames (ignoring the given loader so we can use our
|
||||
own) and return their contents as data structure of nested dicts, lists, etc. If the given
|
||||
filename node's value is a scalar string, then the return value will be a single value. But if
|
||||
the given node value is a list, then the return value will be a list of values, one per loaded
|
||||
configuration file.
|
||||
|
||||
If a filename is relative, probe for it within 1. the current working directory and 2. the given
|
||||
include directory.
|
||||
|
||||
Raise FileNotFoundError if an included file was not found.
|
||||
'''
|
||||
include_directories = [os.getcwd(), os.path.abspath(include_directory)]
|
||||
include_filename = os.path.expanduser(filename_node.value)
|
||||
|
||||
if not os.path.isabs(include_filename):
|
||||
candidate_filenames = [
|
||||
os.path.join(directory, include_filename) for directory in include_directories
|
||||
if isinstance(filename_node.value, str):
|
||||
return probe_and_include_file(filename_node.value, include_directories)
|
||||
|
||||
if (
|
||||
isinstance(filename_node.value, list)
|
||||
and len(filename_node.value)
|
||||
and isinstance(filename_node.value[0], ruamel.yaml.nodes.ScalarNode)
|
||||
):
|
||||
# Reversing the values ensures the correct ordering if these includes are subsequently
|
||||
# merged together.
|
||||
return [
|
||||
probe_and_include_file(node.value, include_directories)
|
||||
for node in reversed(filename_node.value)
|
||||
]
|
||||
|
||||
for candidate_filename in candidate_filenames:
|
||||
if os.path.exists(candidate_filename):
|
||||
include_filename = candidate_filename
|
||||
break
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f'Could not find include {filename_node.value} at {" or ".join(candidate_filenames)}'
|
||||
)
|
||||
|
||||
return load_configuration(include_filename)
|
||||
raise ValueError(
|
||||
'!include value is not supported; use a single filename or a list of filenames'
|
||||
)
|
||||
|
||||
|
||||
def raise_retain_node_error(loader, node):
|
||||
@@ -53,7 +87,7 @@ def raise_retain_node_error(loader, node):
|
||||
'The !retain tag may only be used within a configuration file containing a merged !include tag.'
|
||||
)
|
||||
|
||||
raise ValueError('The !retain tag may only be used on a YAML mapping or sequence.')
|
||||
raise ValueError('The !retain tag may only be used on a mapping or list.')
|
||||
|
||||
|
||||
def raise_omit_node_error(loader, node):
|
||||
@@ -65,14 +99,14 @@ def raise_omit_node_error(loader, node):
|
||||
tags are handled by deep_merge_nodes() below.
|
||||
'''
|
||||
raise ValueError(
|
||||
'The !omit tag may only be used on a scalar (e.g., string) list element within a configuration file containing a merged !include tag.'
|
||||
'The !omit tag may only be used on a scalar (e.g., string) or list element within a configuration file containing a merged !include tag.'
|
||||
)
|
||||
|
||||
|
||||
class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
'''
|
||||
A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including
|
||||
separate YAML configuration files. Example syntax: `retention: !include common.yaml`
|
||||
separate YAML configuration files. Example syntax: `option: !include common.yaml`
|
||||
'''
|
||||
|
||||
def __init__(self, preserve_quotes=None, loader=None, include_directory=None):
|
||||
@@ -81,6 +115,9 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
'!include',
|
||||
functools.partial(include_configuration, include_directory=include_directory),
|
||||
)
|
||||
|
||||
# These are catch-all error handlers for tags that don't get applied and removed by
|
||||
# deep_merge_nodes() below.
|
||||
self.add_constructor('!retain', raise_retain_node_error)
|
||||
self.add_constructor('!omit', raise_omit_node_error)
|
||||
|
||||
@@ -90,8 +127,8 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
using the YAML '<<' merge key. Example syntax:
|
||||
|
||||
```
|
||||
retention:
|
||||
keep_daily: 1
|
||||
option:
|
||||
sub_option: 1
|
||||
|
||||
<<: !include common.yaml
|
||||
```
|
||||
@@ -104,9 +141,15 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
|
||||
for index, (key_node, value_node) in enumerate(node.value):
|
||||
if key_node.tag == u'tag:yaml.org,2002:merge' and value_node.tag == '!include':
|
||||
included_value = representer.represent_data(self.construct_object(value_node))
|
||||
node.value[index] = (key_node, included_value)
|
||||
# Replace the merge include with a sequence of included configuration nodes ready
|
||||
# for merging. The construct_object() call here triggers include_configuration()
|
||||
# among other constructors.
|
||||
node.value[index] = (
|
||||
key_node,
|
||||
representer.represent_data(self.construct_object(value_node)),
|
||||
)
|
||||
|
||||
# This super().flatten_mapping() call actually performs "<<" merges.
|
||||
super(Include_constructor, self).flatten_mapping(node)
|
||||
|
||||
node.value = deep_merge_nodes(node.value)
|
||||
@@ -115,8 +158,7 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
|
||||
def load_configuration(filename):
|
||||
'''
|
||||
Load the given configuration file and return its contents as a data structure of nested dicts
|
||||
and lists. Also, replace any "{constant}" strings with the value of the "constant" key in the
|
||||
"constants" option of the configuration file.
|
||||
and lists.
|
||||
|
||||
Raise ruamel.yaml.error.YAMLError if something goes wrong parsing the YAML, or RecursionError
|
||||
if there are too many recursive includes.
|
||||
@@ -135,67 +177,95 @@ def load_configuration(filename):
|
||||
yaml.Constructor = Include_constructor_with_include_directory
|
||||
|
||||
with open(filename) as file:
|
||||
file_contents = file.read()
|
||||
config = yaml.load(file_contents)
|
||||
|
||||
if config and 'constants' in config:
|
||||
for key, value in config['constants'].items():
|
||||
value = json.dumps(value)
|
||||
file_contents = file_contents.replace(f'{{{key}}}', value.strip('"'))
|
||||
|
||||
config = yaml.load(file_contents)
|
||||
del config['constants']
|
||||
|
||||
return config
|
||||
return yaml.load(file.read())
|
||||
|
||||
|
||||
def filter_omitted_nodes(nodes):
|
||||
def filter_omitted_nodes(nodes, values):
|
||||
'''
|
||||
Given a list of nodes, return a filtered list omitting any nodes with an "!omit" tag or with a
|
||||
value matching such nodes.
|
||||
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
|
||||
|
||||
[
|
||||
(
|
||||
ruamel.yaml.nodes.ScalarNode as a key,
|
||||
ruamel.yaml.nodes.MappingNode or other Node as a value,
|
||||
),
|
||||
...
|
||||
]
|
||||
|
||||
... and a combined list of all values for those nodes, return a filtered list of the values,
|
||||
omitting any that have an "!omit" tag (or with a value matching such nodes).
|
||||
|
||||
But if only a single node is given, bail and return the given values unfiltered, as "!omit" only
|
||||
applies when there are merge includes (and therefore multiple nodes).
|
||||
'''
|
||||
omitted_values = tuple(node.value for node in nodes if node.tag == '!omit')
|
||||
if len(nodes) <= 1:
|
||||
return values
|
||||
|
||||
return [node for node in nodes if node.value not in omitted_values]
|
||||
omitted_values = tuple(node.value for node in values if node.tag == '!omit')
|
||||
|
||||
return [node for node in values if node.value not in omitted_values]
|
||||
|
||||
|
||||
DELETED_NODE = object()
|
||||
def merge_values(nodes):
|
||||
'''
|
||||
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
|
||||
|
||||
[
|
||||
(
|
||||
ruamel.yaml.nodes.ScalarNode as a key,
|
||||
ruamel.yaml.nodes.MappingNode or other Node as a value,
|
||||
),
|
||||
...
|
||||
]
|
||||
|
||||
... merge its sequence or mapping node values and return the result. For sequence nodes, this
|
||||
means appending together its contained lists. For mapping nodes, it means merging its contained
|
||||
dicts.
|
||||
'''
|
||||
return functools.reduce(operator.add, (value.value for key, value in nodes))
|
||||
|
||||
|
||||
def deep_merge_nodes(nodes):
|
||||
'''
|
||||
Given a nested borgmatic configuration data structure as a list of tuples in the form of:
|
||||
|
||||
[
|
||||
(
|
||||
ruamel.yaml.nodes.ScalarNode as a key,
|
||||
ruamel.yaml.nodes.MappingNode or other Node as a value,
|
||||
),
|
||||
...
|
||||
]
|
||||
|
||||
... deep merge any node values corresponding to duplicate keys and return the result. If
|
||||
there are colliding keys with non-MappingNode values (e.g., integers or strings), the last
|
||||
of the values wins.
|
||||
... deep merge any node values corresponding to duplicate keys and return the result. The
|
||||
purpose of merging like this is to support, for instance, merging one borgmatic configuration
|
||||
file into another for reuse, such that a configuration option with sub-options does not
|
||||
completely replace the corresponding option in a merged file.
|
||||
|
||||
If there are colliding keys with scalar values (e.g., integers or strings), the last of the
|
||||
values wins.
|
||||
|
||||
For instance, given node values of:
|
||||
|
||||
[
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
MappingNode(tag='tag:yaml.org,2002:map', value=[
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='keep_hourly'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='24')
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option1'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='1')
|
||||
),
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='keep_daily'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='7')
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='2')
|
||||
),
|
||||
]),
|
||||
),
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
MappingNode(tag='tag:yaml.org,2002:map', value=[
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='keep_daily'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='5')
|
||||
),
|
||||
]),
|
||||
@@ -206,88 +276,95 @@ def deep_merge_nodes(nodes):
|
||||
|
||||
[
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
MappingNode(tag='tag:yaml.org,2002:map', value=[
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='keep_hourly'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='24')
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option1'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='1')
|
||||
),
|
||||
(
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='keep_daily'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:str', value='sub_option2'),
|
||||
ScalarNode(tag='tag:yaml.org,2002:int', value='5')
|
||||
),
|
||||
]),
|
||||
),
|
||||
]
|
||||
|
||||
This function supports multi-way merging, meaning that if the same option name exists three or
|
||||
more times (at the same scope level), all of those instances get merged together.
|
||||
|
||||
If a mapping or sequence node has a YAML "!retain" tag, then that node is not merged.
|
||||
|
||||
The purpose of deep merging like this is to support, for instance, merging one borgmatic
|
||||
configuration file into another for reuse, such that a configuration option with sub-options
|
||||
does not completely replace the corresponding option in a merged file.
|
||||
|
||||
Raise ValueError if a merge is implied using two incompatible types.
|
||||
Raise ValueError if a merge is implied using multiple incompatible types.
|
||||
'''
|
||||
# Map from original node key/value to the replacement merged node. DELETED_NODE as a replacement
|
||||
# node indications deletion.
|
||||
replaced_nodes = {}
|
||||
merged_nodes = []
|
||||
|
||||
# To find nodes that require merging, compare each node with each other node.
|
||||
for a_key, a_value in nodes:
|
||||
for b_key, b_value in nodes:
|
||||
# If we've already considered one of the nodes for merging, skip it.
|
||||
if (a_key, a_value) in replaced_nodes or (b_key, b_value) in replaced_nodes:
|
||||
continue
|
||||
def get_node_key_name(node):
|
||||
return node[0].value
|
||||
|
||||
# If the keys match and the values are different, we need to merge these two A and B nodes.
|
||||
if a_key.tag == b_key.tag and a_key.value == b_key.value and a_value != b_value:
|
||||
if not type(a_value) is type(b_value):
|
||||
raise ValueError(
|
||||
f'Incompatible types found when trying to merge "{a_key.value}:" values across configuration files: {type(a_value).id} and {type(b_value).id}'
|
||||
# Bucket the nodes by their keys. Then merge all of the values sharing the same key.
|
||||
for key_name, grouped_nodes in itertools.groupby(
|
||||
sorted(nodes, key=get_node_key_name), get_node_key_name
|
||||
):
|
||||
grouped_nodes = list(grouped_nodes)
|
||||
|
||||
# The merged node inherits its attributes from the final node in the group.
|
||||
(last_node_key, last_node_value) = grouped_nodes[-1]
|
||||
value_types = set(type(value) for (_, value) in grouped_nodes)
|
||||
|
||||
if len(value_types) > 1:
|
||||
raise ValueError(
|
||||
f'Incompatible types found when trying to merge "{key_name}:" values across configuration files: {", ".join(value_type.id for value_type in value_types)}'
|
||||
)
|
||||
|
||||
# If we're dealing with MappingNodes, recurse and merge its values as well.
|
||||
if ruamel.yaml.nodes.MappingNode in value_types:
|
||||
# A "!retain" tag says to skip deep merging for this node. Replace the tag so
|
||||
# downstream schema validation doesn't break on our application-specific tag.
|
||||
if last_node_value.tag == '!retain' and len(grouped_nodes) > 1:
|
||||
last_node_value.tag = 'tag:yaml.org,2002:map'
|
||||
merged_nodes.append((last_node_key, last_node_value))
|
||||
else:
|
||||
merged_nodes.append(
|
||||
(
|
||||
last_node_key,
|
||||
ruamel.yaml.nodes.MappingNode(
|
||||
tag=last_node_value.tag,
|
||||
value=deep_merge_nodes(merge_values(grouped_nodes)),
|
||||
start_mark=last_node_value.start_mark,
|
||||
end_mark=last_node_value.end_mark,
|
||||
flow_style=last_node_value.flow_style,
|
||||
comment=last_node_value.comment,
|
||||
anchor=last_node_value.anchor,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Since we're merging into the B node, consider the A node a duplicate and remove it.
|
||||
replaced_nodes[(a_key, a_value)] = DELETED_NODE
|
||||
continue
|
||||
|
||||
# If we're dealing with MappingNodes, recurse and merge its values as well.
|
||||
if isinstance(b_value, ruamel.yaml.nodes.MappingNode):
|
||||
# A "!retain" tag says to skip deep merging for this node. Replace the tag so
|
||||
# downstream schema validation doesn't break on our application-specific tag.
|
||||
if b_value.tag == '!retain':
|
||||
b_value.tag = 'tag:yaml.org,2002:map'
|
||||
else:
|
||||
replaced_nodes[(b_key, b_value)] = (
|
||||
b_key,
|
||||
ruamel.yaml.nodes.MappingNode(
|
||||
tag=b_value.tag,
|
||||
value=deep_merge_nodes(a_value.value + b_value.value),
|
||||
start_mark=b_value.start_mark,
|
||||
end_mark=b_value.end_mark,
|
||||
flow_style=b_value.flow_style,
|
||||
comment=b_value.comment,
|
||||
anchor=b_value.anchor,
|
||||
),
|
||||
)
|
||||
# If we're dealing with SequenceNodes, merge by appending one sequence to the other.
|
||||
elif isinstance(b_value, ruamel.yaml.nodes.SequenceNode):
|
||||
# A "!retain" tag says to skip deep merging for this node. Replace the tag so
|
||||
# downstream schema validation doesn't break on our application-specific tag.
|
||||
if b_value.tag == '!retain':
|
||||
b_value.tag = 'tag:yaml.org,2002:seq'
|
||||
else:
|
||||
replaced_nodes[(b_key, b_value)] = (
|
||||
b_key,
|
||||
ruamel.yaml.nodes.SequenceNode(
|
||||
tag=b_value.tag,
|
||||
value=filter_omitted_nodes(a_value.value + b_value.value),
|
||||
start_mark=b_value.start_mark,
|
||||
end_mark=b_value.end_mark,
|
||||
flow_style=b_value.flow_style,
|
||||
comment=b_value.comment,
|
||||
anchor=b_value.anchor,
|
||||
),
|
||||
)
|
||||
# If we're dealing with SequenceNodes, merge by appending sequences together.
|
||||
if ruamel.yaml.nodes.SequenceNode in value_types:
|
||||
if last_node_value.tag == '!retain' and len(grouped_nodes) > 1:
|
||||
last_node_value.tag = 'tag:yaml.org,2002:seq'
|
||||
merged_nodes.append((last_node_key, last_node_value))
|
||||
else:
|
||||
merged_nodes.append(
|
||||
(
|
||||
last_node_key,
|
||||
ruamel.yaml.nodes.SequenceNode(
|
||||
tag=last_node_value.tag,
|
||||
value=filter_omitted_nodes(grouped_nodes, merge_values(grouped_nodes)),
|
||||
start_mark=last_node_value.start_mark,
|
||||
end_mark=last_node_value.end_mark,
|
||||
flow_style=last_node_value.flow_style,
|
||||
comment=last_node_value.comment,
|
||||
anchor=last_node_value.anchor,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE
|
||||
]
|
||||
continue
|
||||
|
||||
merged_nodes.append((last_node_key, last_node_value))
|
||||
|
||||
return merged_nodes
|
||||
|
||||
@@ -10,7 +10,11 @@ def normalize_sections(config_filename, config):
|
||||
|
||||
Raise ValueError if the "prefix" option is set in both "location" and "consistency" sections.
|
||||
'''
|
||||
location = config.get('location') or {}
|
||||
try:
|
||||
location = config.get('location') or {}
|
||||
except AttributeError:
|
||||
raise ValueError('Configuration does not contain any options')
|
||||
|
||||
storage = config.get('storage') or {}
|
||||
consistency = config.get('consistency') or {}
|
||||
hooks = config.get('hooks') or {}
|
||||
@@ -35,7 +39,7 @@ def normalize_sections(config_filename, config):
|
||||
for section_name in ('location', 'storage', 'retention', 'consistency', 'output', 'hooks'):
|
||||
section_config = config.get(section_name)
|
||||
|
||||
if section_config:
|
||||
if section_config is not None:
|
||||
any_section_upgraded = True
|
||||
del config[section_name]
|
||||
config.update(section_config)
|
||||
@@ -46,7 +50,7 @@ def normalize_sections(config_filename, config):
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: Configuration sections (like location: and storage:) are deprecated and support will be removed from a future release. To prepare for this, move your options out of sections to the global scope.',
|
||||
msg=f'{config_filename}: Configuration sections (like location:, storage:, retention:, consistency:, and hooks:) are deprecated and support will be removed from a future release. To prepare for this, move your options out of sections to the global scope.',
|
||||
)
|
||||
)
|
||||
]
|
||||
@@ -86,7 +90,7 @@ def normalize(config_filename, config):
|
||||
dict(
|
||||
levelno=logging.WARNING,
|
||||
levelname='WARNING',
|
||||
msg=f'{config_filename}: The healthchecks hook now expects a mapping value. String values for this option are deprecated and support will be removed from a future release.',
|
||||
msg=f'{config_filename}: The healthchecks hook now expects a key/value pair with "ping_url" as a key. String values for this option are deprecated and support will be removed from a future release.',
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -22,13 +22,19 @@ def set_values(config, keys, value):
|
||||
set_values(config[first_key], keys[1:], value)
|
||||
|
||||
|
||||
def convert_value_type(value):
|
||||
def convert_value_type(value, option_type):
|
||||
'''
|
||||
Given a string value, determine its logical type (string, boolean, integer, etc.), and return it
|
||||
converted to that type.
|
||||
Given a string value and its schema type as a string, determine its logical type (string,
|
||||
boolean, integer, etc.), and return it converted to that type.
|
||||
|
||||
If the option type is a string, leave the value as a string so that special characters in it
|
||||
don't get interpreted as YAML during conversion.
|
||||
|
||||
Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
|
||||
'''
|
||||
if option_type == 'string':
|
||||
return value
|
||||
|
||||
return ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
|
||||
|
||||
|
||||
@@ -46,11 +52,32 @@ def strip_section_names(parsed_override_key):
|
||||
return parsed_override_key
|
||||
|
||||
|
||||
def parse_overrides(raw_overrides):
|
||||
def type_for_option(schema, option_keys):
|
||||
'''
|
||||
Given a sequence of configuration file override strings in the form of "option.suboption=value",
|
||||
parse and return a sequence of tuples (keys, values), where keys is a sequence of strings. For
|
||||
instance, given the following raw overrides:
|
||||
Given a configuration schema and a sequence of keys identifying an option, e.g.
|
||||
('extra_borg_options', 'init'), return the schema type of that option as a string.
|
||||
|
||||
Return None if the option or its type cannot be found in the schema.
|
||||
'''
|
||||
option_schema = schema
|
||||
|
||||
for key in option_keys:
|
||||
try:
|
||||
option_schema = option_schema['properties'][key]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
try:
|
||||
return option_schema['type']
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def parse_overrides(raw_overrides, schema):
|
||||
'''
|
||||
Given a sequence of configuration file override strings in the form of "option.suboption=value"
|
||||
and a configuration schema dict, parse and return a sequence of tuples (keys, values), where
|
||||
keys is a sequence of strings. For instance, given the following raw overrides:
|
||||
|
||||
['my_option.suboption=value1', 'other_option=value2']
|
||||
|
||||
@@ -71,10 +98,13 @@ def parse_overrides(raw_overrides):
|
||||
for raw_override in raw_overrides:
|
||||
try:
|
||||
raw_keys, value = raw_override.split('=', 1)
|
||||
keys = strip_section_names(tuple(raw_keys.split('.')))
|
||||
option_type = type_for_option(schema, keys)
|
||||
|
||||
parsed_overrides.append(
|
||||
(
|
||||
strip_section_names(tuple(raw_keys.split('.'))),
|
||||
convert_value_type(value),
|
||||
keys,
|
||||
convert_value_type(value, option_type),
|
||||
)
|
||||
)
|
||||
except ValueError:
|
||||
@@ -87,12 +117,13 @@ def parse_overrides(raw_overrides):
|
||||
return tuple(parsed_overrides)
|
||||
|
||||
|
||||
def apply_overrides(config, raw_overrides):
|
||||
def apply_overrides(config, schema, raw_overrides):
|
||||
'''
|
||||
Given a configuration dict and a sequence of configuration file override strings in the form of
|
||||
"option.suboption=value", parse each override and set it the configuration dict.
|
||||
Given a configuration dict, a corresponding configuration schema dict, and a sequence of
|
||||
configuration file override strings in the form of "option.suboption=value", parse each override
|
||||
and set it into the configuration dict.
|
||||
'''
|
||||
overrides = parse_overrides(raw_overrides)
|
||||
overrides = parse_overrides(raw_overrides, schema)
|
||||
|
||||
for keys, value in overrides:
|
||||
set_values(config, keys, value)
|
||||
|
||||
+303
-45
@@ -7,7 +7,7 @@ properties:
|
||||
type: object
|
||||
description: |
|
||||
Constants to use in the configuration file. All occurrences of the
|
||||
constant name within culy braces will be replaced with the value.
|
||||
constant name within curly braces will be replaced with the value.
|
||||
For example, if you have a constant named "hostname" with the value
|
||||
"myhostname", then the string "{hostname}" will be replaced with
|
||||
"myhostname" in the configuration file.
|
||||
@@ -19,8 +19,8 @@ properties:
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of source directories and files to backup. Globs and tildes are
|
||||
expanded. Do not backslash spaces in path names.
|
||||
List of source directories and files to back up. Globs and tildes
|
||||
are expanded. Do not backslash spaces in path names.
|
||||
example:
|
||||
- /home
|
||||
- /etc
|
||||
@@ -29,7 +29,7 @@ properties:
|
||||
repositories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- path
|
||||
properties:
|
||||
@@ -210,6 +210,13 @@ properties:
|
||||
"borgmatic restore" from finding any database dumps created before
|
||||
the change. Defaults to ~/.borgmatic
|
||||
example: /tmp/borgmatic
|
||||
store_config_files:
|
||||
type: boolean
|
||||
description: |
|
||||
Store configuration files used to create a backup in the backup
|
||||
itself. Defaults to true. Changing this to false prevents "borgmatic
|
||||
bootstrap" from extracting configuration files from the backup.
|
||||
example: false
|
||||
source_directories_must_exist:
|
||||
type: boolean
|
||||
description: |
|
||||
@@ -253,7 +260,7 @@ properties:
|
||||
chunker_params:
|
||||
type: string
|
||||
description: |
|
||||
Specify the parameters passed to then chunker (CHUNK_MIN_EXP,
|
||||
Specify the parameters passed to the 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".
|
||||
@@ -280,20 +287,23 @@ properties:
|
||||
retry_wait:
|
||||
type: integer
|
||||
description: |
|
||||
Wait time between retries (in seconds) to allow transient issues to
|
||||
pass. Increases after each retry as a form of backoff. Defaults to 0
|
||||
(no wait).
|
||||
Wait time between retries (in seconds) to allow transient issues
|
||||
to pass. Increases after each retry by that same wait time as a
|
||||
form of backoff. Defaults to 0 (no wait).
|
||||
example: 10
|
||||
temporary_directory:
|
||||
type: string
|
||||
description: |
|
||||
Directory where temporary files are stored. Defaults to $TMPDIR.
|
||||
Directory where temporary Borg files are stored. Defaults to
|
||||
$TMPDIR. See "Resource Usage" at
|
||||
https://borgbackup.readthedocs.io/en/stable/usage/general.html for
|
||||
details.
|
||||
example: /path/to/tmpdir
|
||||
ssh_command:
|
||||
type: string
|
||||
description: |
|
||||
Command to use instead of "ssh". This can be used to specify ssh
|
||||
options. Defaults to not set.
|
||||
options. Defaults to not set.
|
||||
example: ssh -i /path/to/private/key
|
||||
borg_base_directory:
|
||||
type: string
|
||||
@@ -365,19 +375,19 @@ properties:
|
||||
type: boolean
|
||||
description: |
|
||||
Bypass Borg error about a repository that has been moved. Defaults
|
||||
to false.
|
||||
to not bypassing.
|
||||
example: true
|
||||
unknown_unencrypted_repo_access_is_ok:
|
||||
type: boolean
|
||||
description: |
|
||||
Bypass Borg error about a previously unknown unencrypted repository.
|
||||
Defaults to false.
|
||||
Defaults to not bypassing.
|
||||
example: true
|
||||
check_i_know_what_i_am_doing:
|
||||
type: boolean
|
||||
description: |
|
||||
Bypass Borg confirmation about check with repair option.
|
||||
Defaults to false.
|
||||
Bypass Borg confirmation about check with repair option. Defaults to
|
||||
an interactive prompt from Borg.
|
||||
example: true
|
||||
extra_borg_options:
|
||||
type: object
|
||||
@@ -416,7 +426,9 @@ properties:
|
||||
command-line invocation.
|
||||
keep_within:
|
||||
type: string
|
||||
description: Keep all archives within this time interval.
|
||||
description: |
|
||||
Keep all archives within this time interval. See "skip_actions" for
|
||||
disabling pruning altogether.
|
||||
example: 3H
|
||||
keep_secondly:
|
||||
type: integer
|
||||
@@ -472,13 +484,13 @@ properties:
|
||||
- disabled
|
||||
description: |
|
||||
Name of consistency check to run: "repository",
|
||||
"archives", "data", and/or "extract". 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".
|
||||
"archives", "data", and/or "extract". "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". See "skip_actions"
|
||||
for disabling checks altogether.
|
||||
example: repository
|
||||
frequency:
|
||||
type: string
|
||||
@@ -518,6 +530,18 @@ properties:
|
||||
Apply color to console output. Can be overridden with --no-color
|
||||
command-line flag. Defaults to true.
|
||||
example: false
|
||||
skip_actions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
List of one or more actions to skip running for this configuration
|
||||
file, even if specified on the command-line (explicitly or
|
||||
implicitly). This is handy for append-only configurations where you
|
||||
never want to run "compact" or checkless configuration where you
|
||||
want to skip "check". Defaults to not skipping any actions.
|
||||
example:
|
||||
- compact
|
||||
before_actions:
|
||||
type: array
|
||||
items:
|
||||
@@ -834,10 +858,121 @@ properties:
|
||||
description: |
|
||||
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 removed
|
||||
afterwards. Requires pg_dump/pg_dumpall/pg_restore commands. See
|
||||
added to your source directories at runtime and streamed directly
|
||||
to Borg. Requires pg_dump/pg_dumpall/pg_restore commands. See
|
||||
https://www.postgresql.org/docs/current/app-pgdump.html and
|
||||
https://www.postgresql.org/docs/current/libpq-ssl.html for details.
|
||||
https://www.postgresql.org/docs/current/libpq-ssl.html for
|
||||
details.
|
||||
mariadb_databases:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: ['name']
|
||||
additionalProperties: false
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: |
|
||||
Database name (required if using this hook). Or "all" to
|
||||
dump all databases on the host. Note that using this
|
||||
database hook implicitly enables both read_special and
|
||||
one_file_system (see above) to support dump and restore
|
||||
streaming.
|
||||
example: users
|
||||
hostname:
|
||||
type: string
|
||||
description: |
|
||||
Database hostname to connect to. Defaults to connecting
|
||||
via local Unix socket.
|
||||
example: database.example.org
|
||||
restore_hostname:
|
||||
type: string
|
||||
description: |
|
||||
Database hostname to restore to. Defaults to the
|
||||
"hostname" option.
|
||||
example: database.example.org
|
||||
port:
|
||||
type: integer
|
||||
description: Port to connect to. Defaults to 3306.
|
||||
example: 3307
|
||||
restore_port:
|
||||
type: integer
|
||||
description: |
|
||||
Port to restore to. Defaults to the "port" option.
|
||||
example: 5433
|
||||
username:
|
||||
type: string
|
||||
description: |
|
||||
Username with which to connect to the database. Defaults
|
||||
to the username of the current user.
|
||||
example: dbuser
|
||||
restore_username:
|
||||
type: string
|
||||
description: |
|
||||
Username with which to restore the database. Defaults to
|
||||
the "username" option.
|
||||
example: dbuser
|
||||
password:
|
||||
type: string
|
||||
description: |
|
||||
Password with which to connect to the database. Omitting
|
||||
a password will only work if MariaDB is configured to
|
||||
trust the configured username without a password.
|
||||
example: trustsome1
|
||||
restore_password:
|
||||
type: string
|
||||
description: |
|
||||
Password with which to connect to the restore database.
|
||||
Defaults to the "password" option.
|
||||
example: trustsome1
|
||||
format:
|
||||
type: string
|
||||
enum: ['sql']
|
||||
description: |
|
||||
Database dump output format. Currently only "sql" is
|
||||
supported. Defaults to "sql" for a single database. Or,
|
||||
when database name is "all" and format is blank, dumps
|
||||
all databases to a single file. But if a format is
|
||||
specified with an "all" database name, dumps each
|
||||
database to a separate file of that format, allowing
|
||||
more convenient restores of individual databases.
|
||||
example: directory
|
||||
add_drop_database:
|
||||
type: boolean
|
||||
description: |
|
||||
Use the "--add-drop-database" flag with mariadb-dump,
|
||||
causing the database to be dropped right before restore.
|
||||
Defaults to true.
|
||||
example: false
|
||||
options:
|
||||
type: string
|
||||
description: |
|
||||
Additional mariadb-dump options to pass directly to the
|
||||
dump command, without performing any validation on them.
|
||||
See mariadb-dump documentation for details.
|
||||
example: --skip-comments
|
||||
list_options:
|
||||
type: string
|
||||
description: |
|
||||
Additional options to pass directly to the mariadb
|
||||
command that lists available databases, without
|
||||
performing any validation on them. See mariadb command
|
||||
documentation for details.
|
||||
example: --defaults-extra-file=mariadb.cnf
|
||||
restore_options:
|
||||
type: string
|
||||
description: |
|
||||
Additional options to pass directly to the mariadb
|
||||
command that restores database dumps, without
|
||||
performing any validation on them. See mariadb command
|
||||
documentation for details.
|
||||
example: --defaults-extra-file=mariadb.cnf
|
||||
description: |
|
||||
List of one or more MariaDB databases to dump before creating a
|
||||
backup, run once per configuration file. The database dumps are
|
||||
added to your source directories at runtime and streamed directly
|
||||
to Borg. Requires mariadb-dump/mariadb commands. See
|
||||
https://mariadb.com/kb/en/library/mysqldump/ for details.
|
||||
mysql_databases:
|
||||
type: array
|
||||
items:
|
||||
@@ -886,7 +1021,7 @@ properties:
|
||||
description: |
|
||||
Username with which to restore the database. Defaults to
|
||||
the "username" option.
|
||||
example: dbuser
|
||||
example: dbuser
|
||||
password:
|
||||
type: string
|
||||
description: |
|
||||
@@ -899,7 +1034,7 @@ properties:
|
||||
description: |
|
||||
Password with which to connect to the restore database.
|
||||
Defaults to the "password" option.
|
||||
example: trustsome1
|
||||
example: trustsome1
|
||||
format:
|
||||
type: string
|
||||
enum: ['sql']
|
||||
@@ -929,26 +1064,26 @@ properties:
|
||||
list_options:
|
||||
type: string
|
||||
description: |
|
||||
Additional mysql options to pass directly to the mysql
|
||||
Additional options to pass directly to the mysql
|
||||
command that lists available databases, without
|
||||
performing any validation on them. See mysql
|
||||
performing any validation on them. See mysql command
|
||||
documentation for details.
|
||||
example: --defaults-extra-file=my.cnf
|
||||
restore_options:
|
||||
type: string
|
||||
description: |
|
||||
Additional mysql options to pass directly to the mysql
|
||||
command that restores database dumps, without performing
|
||||
any validation on them. See mysql documentation for
|
||||
details.
|
||||
Additional options to pass directly to the mysql
|
||||
command that restores database dumps, without
|
||||
performing any validation on them. See mysql command
|
||||
documentation for details.
|
||||
example: --defaults-extra-file=my.cnf
|
||||
description: |
|
||||
List of one or more MySQL/MariaDB 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 removed
|
||||
afterwards. Requires mysqldump/mysql commands (from either MySQL or
|
||||
MariaDB). See https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html
|
||||
or https://mariadb.com/kb/en/library/mysqldump/ for details.
|
||||
List of one or more MySQL databases to dump before creating a
|
||||
backup, run once per configuration file. The database dumps are
|
||||
added to your source directories at runtime and streamed directly
|
||||
to Borg. Requires mysqldump/mysql commands. See
|
||||
https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html for
|
||||
details.
|
||||
sqlite_databases:
|
||||
type: array
|
||||
items:
|
||||
@@ -1014,7 +1149,7 @@ properties:
|
||||
type: integer
|
||||
description: |
|
||||
Port to restore to. Defaults to the "port" option.
|
||||
example: 5433
|
||||
example: 5433
|
||||
username:
|
||||
type: string
|
||||
description: |
|
||||
@@ -1026,7 +1161,7 @@ properties:
|
||||
description: |
|
||||
Username with which to restore the database. Defaults to
|
||||
the "username" option.
|
||||
example: dbuser
|
||||
example: dbuser
|
||||
password:
|
||||
type: string
|
||||
description: |
|
||||
@@ -1073,8 +1208,8 @@ properties:
|
||||
description: |
|
||||
List of one or more MongoDB 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 removed
|
||||
afterwards. Requires mongodump/mongorestore commands. See
|
||||
added to your source directories at runtime and streamed directly
|
||||
to Borg. Requires mongodump/mongorestore commands. See
|
||||
https://docs.mongodb.com/database-tools/mongodump/ and
|
||||
https://docs.mongodb.com/database-tools/mongorestore/ for details.
|
||||
ntfy:
|
||||
@@ -1188,6 +1323,99 @@ properties:
|
||||
example:
|
||||
- start
|
||||
- finish
|
||||
apprise:
|
||||
type: object
|
||||
required: ['services']
|
||||
additionalProperties: false
|
||||
properties:
|
||||
services:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required:
|
||||
- url
|
||||
- label
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
example: "gotify://hostname/token"
|
||||
label:
|
||||
type: string
|
||||
example: gotify
|
||||
description: |
|
||||
A list of Apprise services to publish to with URLs and
|
||||
labels. The labels are used for logging. A full list of
|
||||
services and their configuration can be found at
|
||||
https://github.com/caronc/apprise/wiki.
|
||||
example:
|
||||
- url: "kodi://user@hostname"
|
||||
label: kodi
|
||||
- url: "line://Token@User"
|
||||
label: line
|
||||
start:
|
||||
type: object
|
||||
required: ['body']
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message title. If left unspecified, no
|
||||
title is sent.
|
||||
example: Ping!
|
||||
body:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message body.
|
||||
example: Starting backup process.
|
||||
finish:
|
||||
type: object
|
||||
required: ['body']
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message title. If left unspecified, no
|
||||
title is sent.
|
||||
example: Ping!
|
||||
body:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message body.
|
||||
example: Backups successfully made.
|
||||
fail:
|
||||
type: object
|
||||
required: ['body']
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message title. If left unspecified, no
|
||||
title is sent.
|
||||
example: Ping!
|
||||
body:
|
||||
type: string
|
||||
description: |
|
||||
Specify the message body.
|
||||
example: Your backups have failed.
|
||||
states:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- start
|
||||
- finish
|
||||
- fail
|
||||
uniqueItems: true
|
||||
description: |
|
||||
List of one or more monitoring states to ping for: "start",
|
||||
"finish", and/or "fail". Defaults to pinging for failure
|
||||
only. For each selected state, corresponding configuration
|
||||
for the message title and body should be given. If any is
|
||||
left unspecified, a generic message is emitted instead.
|
||||
example:
|
||||
- start
|
||||
- finish
|
||||
|
||||
healthchecks:
|
||||
type: object
|
||||
required: ['ping_url']
|
||||
@@ -1268,7 +1496,7 @@ properties:
|
||||
example: a177cad45bd374409f78906a810a3074
|
||||
description: |
|
||||
Configuration for a monitoring integration with PagerDuty. Create an
|
||||
account at https://www.pagerduty.com/ if you'd like to use this
|
||||
account at https://www.pagerduty.com if you'd like to use this
|
||||
service. See borgmatic monitoring documentation for details.
|
||||
cronhub:
|
||||
type: object
|
||||
@@ -1282,6 +1510,36 @@ properties:
|
||||
ends, or errors.
|
||||
example: https://cronhub.io/ping/1f5e3410-254c-5587
|
||||
description: |
|
||||
Configuration for a monitoring integration with Crunhub. Create an
|
||||
Configuration for a monitoring integration with Cronhub. Create an
|
||||
account at https://cronhub.io if you'd like to use this service. See
|
||||
borgmatic monitoring documentation for details.
|
||||
loki:
|
||||
type: object
|
||||
required: ['url', 'labels']
|
||||
additionalProperties: false
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
description: |
|
||||
Grafana loki log URL to notify when a backup begins,
|
||||
ends, or fails.
|
||||
example: "http://localhost:3100/loki/api/v1/push"
|
||||
labels:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: |
|
||||
Allows setting custom labels for the logging stream. At
|
||||
least one label is required. "__hostname" gets replaced by
|
||||
the machine hostname automatically. "__config" gets replaced
|
||||
by just the name of the configuration file. "__config_path"
|
||||
gets replaced by the full path of the configuration file.
|
||||
example:
|
||||
app: "borgmatic"
|
||||
config: "__config"
|
||||
hostname: "__hostname"
|
||||
description: |
|
||||
Configuration for a monitoring integration with Grafana loki. You
|
||||
can send the logs to a self-hosted instance or create an account at
|
||||
https://grafana.com/auth/sign-up/create-user. See borgmatic
|
||||
monitoring documentation for details.
|
||||
|
||||
@@ -4,7 +4,7 @@ import jsonschema
|
||||
import ruamel.yaml
|
||||
|
||||
import borgmatic.config
|
||||
from borgmatic.config import environment, load, normalize, override
|
||||
from borgmatic.config import constants, environment, load, normalize, override
|
||||
|
||||
|
||||
def schema_filename():
|
||||
@@ -109,11 +109,14 @@ def parse_configuration(config_filename, schema_filename, overrides=None, resolv
|
||||
except (ruamel.yaml.error.YAMLError, RecursionError) as error:
|
||||
raise Validation_error(config_filename, (str(error),))
|
||||
|
||||
override.apply_overrides(config, overrides)
|
||||
logs = normalize.normalize(config_filename, config)
|
||||
override.apply_overrides(config, schema, overrides)
|
||||
constants.apply_constants(config, config.get('constants') if config else {})
|
||||
|
||||
if resolve_env:
|
||||
environment.resolve_env_variables(config)
|
||||
|
||||
logs = normalize.normalize(config_filename, config)
|
||||
|
||||
try:
|
||||
validator = jsonschema.Draft7Validator(schema)
|
||||
except AttributeError: # pragma: no cover
|
||||
|
||||
@@ -134,6 +134,7 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
|
||||
still_running = True
|
||||
|
||||
command = process.args.split(' ') if isinstance(process.args, str) else process.args
|
||||
|
||||
# If any process errors, then raise accordingly.
|
||||
if exit_code_indicates_error(command, exit_code, borg_local_path):
|
||||
# If an error occurs, include its output in the raised exception so that we don't
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import logging
|
||||
import operator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def initialize_monitor(
|
||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
No initialization is necessary for this monitor.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Ping the configured Apprise service URLs. Use the given configuration filename in any log
|
||||
entries. If this is a dry run, then don't actually ping anything.
|
||||
'''
|
||||
try:
|
||||
import apprise
|
||||
from apprise import NotifyFormat, NotifyType
|
||||
except ImportError: # pragma: no cover
|
||||
logger.warning('Unable to import Apprise in monitoring hook')
|
||||
return
|
||||
|
||||
state_to_notify_type = {
|
||||
'start': NotifyType.INFO,
|
||||
'finish': NotifyType.SUCCESS,
|
||||
'fail': NotifyType.FAILURE,
|
||||
'log': NotifyType.INFO,
|
||||
}
|
||||
|
||||
run_states = hook_config.get('states', ['fail'])
|
||||
|
||||
if state.name.lower() not in run_states:
|
||||
return
|
||||
|
||||
state_config = hook_config.get(
|
||||
state.name.lower(),
|
||||
{
|
||||
'title': f'A borgmatic {state.name} event happened',
|
||||
'body': f'A borgmatic {state.name} event happened',
|
||||
},
|
||||
)
|
||||
|
||||
if not hook_config.get('services'):
|
||||
logger.info(f'{config_filename}: No Apprise services to ping')
|
||||
return
|
||||
|
||||
dry_run_string = ' (dry run; not actually pinging)' if dry_run else ''
|
||||
labels_string = ', '.join(map(operator.itemgetter('label'), hook_config.get('services')))
|
||||
logger.info(f'{config_filename}: Pinging Apprise services: {labels_string}{dry_run_string}')
|
||||
|
||||
apprise_object = apprise.Apprise()
|
||||
apprise_object.add(list(map(operator.itemgetter('url'), hook_config.get('services'))))
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
result = apprise_object.notify(
|
||||
title=state_config.get('title', ''),
|
||||
body=state_config.get('body'),
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=state_to_notify_type[state.name.lower()],
|
||||
)
|
||||
|
||||
if result is False:
|
||||
logger.warning(f'{config_filename}: Error sending some Apprise notifications')
|
||||
|
||||
|
||||
def destroy_monitor(
|
||||
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
|
||||
): # pragma: no cover
|
||||
'''
|
||||
No destruction is necessary for this monitor.
|
||||
'''
|
||||
pass
|
||||
@@ -1,9 +1,12 @@
|
||||
import logging
|
||||
|
||||
from borgmatic.hooks import (
|
||||
apprise,
|
||||
cronhub,
|
||||
cronitor,
|
||||
healthchecks,
|
||||
loki,
|
||||
mariadb,
|
||||
mongodb,
|
||||
mysql,
|
||||
ntfy,
|
||||
@@ -15,15 +18,18 @@ from borgmatic.hooks import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HOOK_NAME_TO_MODULE = {
|
||||
'apprise': apprise,
|
||||
'cronhub': cronhub,
|
||||
'cronitor': cronitor,
|
||||
'healthchecks': healthchecks,
|
||||
'mariadb_databases': mariadb,
|
||||
'mongodb_databases': mongodb,
|
||||
'mysql_databases': mysql,
|
||||
'ntfy': ntfy,
|
||||
'pagerduty': pagerduty,
|
||||
'postgresql_databases': postgresql,
|
||||
'sqlite_databases': sqlite,
|
||||
'loki': loki,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+16
-15
@@ -6,34 +6,35 @@ from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATABASE_HOOK_NAMES = (
|
||||
'postgresql_databases',
|
||||
DATA_SOURCE_HOOK_NAMES = (
|
||||
'mariadb_databases',
|
||||
'mysql_databases',
|
||||
'mongodb_databases',
|
||||
'postgresql_databases',
|
||||
'sqlite_databases',
|
||||
)
|
||||
|
||||
|
||||
def make_database_dump_path(borgmatic_source_directory, database_hook_name):
|
||||
def make_data_source_dump_path(borgmatic_source_directory, data_source_hook_name):
|
||||
'''
|
||||
Given a borgmatic source directory (or None) and a database hook name, construct a database dump
|
||||
path.
|
||||
Given a borgmatic source directory (or None) and a data source hook name, construct a data
|
||||
source dump path.
|
||||
'''
|
||||
if not borgmatic_source_directory:
|
||||
borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||
|
||||
return os.path.join(borgmatic_source_directory, database_hook_name)
|
||||
return os.path.join(borgmatic_source_directory, data_source_hook_name)
|
||||
|
||||
|
||||
def make_database_dump_filename(dump_path, name, hostname=None):
|
||||
def make_data_source_dump_filename(dump_path, name, hostname=None):
|
||||
'''
|
||||
Based on the given dump directory path, database name, and hostname, return a filename to use
|
||||
for the database dump. The hostname defaults to localhost.
|
||||
Based on the given dump directory path, data source name, and hostname, return a filename to use
|
||||
for the data source dump. The hostname defaults to localhost.
|
||||
|
||||
Raise ValueError if the database name is invalid.
|
||||
Raise ValueError if the data source name is invalid.
|
||||
'''
|
||||
if os.path.sep in name:
|
||||
raise ValueError(f'Invalid database name {name}')
|
||||
raise ValueError(f'Invalid data source name {name}')
|
||||
|
||||
return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
|
||||
|
||||
@@ -53,14 +54,14 @@ def create_named_pipe_for_dump(dump_path):
|
||||
os.mkfifo(dump_path, mode=0o600)
|
||||
|
||||
|
||||
def remove_database_dumps(dump_path, database_type_name, log_prefix, dry_run):
|
||||
def remove_data_source_dumps(dump_path, data_source_type_name, log_prefix, dry_run):
|
||||
'''
|
||||
Remove all database dumps in the given dump directory path (including the directory itself). If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
Remove all data source dumps in the given dump directory path (including the directory itself).
|
||||
If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
|
||||
|
||||
logger.debug(f'{log_prefix}: Removing {database_type_name} database dumps{dry_run_label}')
|
||||
logger.debug(f'{log_prefix}: Removing {data_source_type_name} data source dumps{dry_run_label}')
|
||||
|
||||
expanded_path = os.path.expanduser(dump_path)
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from borgmatic.hooks import monitor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MONITOR_STATE_TO_LOKI = {
|
||||
monitor.State.START: 'Started',
|
||||
monitor.State.FINISH: 'Finished',
|
||||
monitor.State.FAIL: 'Failed',
|
||||
}
|
||||
|
||||
# Threshold at which logs get flushed to loki
|
||||
MAX_BUFFER_LINES = 100
|
||||
|
||||
|
||||
class Loki_log_buffer:
|
||||
'''
|
||||
A log buffer that allows to output the logs as loki requests in json. Allows
|
||||
adding labels to the log stream and takes care of communication with loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, dry_run):
|
||||
self.url = url
|
||||
self.dry_run = dry_run
|
||||
self.root = {'streams': [{'stream': {}, 'values': []}]}
|
||||
|
||||
def add_value(self, value):
|
||||
'''
|
||||
Add a log entry to the stream.
|
||||
'''
|
||||
timestamp = str(time.time_ns())
|
||||
self.root['streams'][0]['values'].append((timestamp, value))
|
||||
|
||||
def add_label(self, label, value):
|
||||
'''
|
||||
Add a label to the logging stream.
|
||||
'''
|
||||
self.root['streams'][0]['stream'][label] = value
|
||||
|
||||
def to_request(self):
|
||||
return json.dumps(self.root)
|
||||
|
||||
def __len__(self):
|
||||
'''
|
||||
Gets the number of lines currently in the buffer.
|
||||
'''
|
||||
return len(self.root['streams'][0]['values'])
|
||||
|
||||
def flush(self):
|
||||
if self.dry_run:
|
||||
# Just empty the buffer and skip
|
||||
self.root['streams'][0]['values'] = []
|
||||
logger.info('Skipped uploading logs to loki due to dry run')
|
||||
return
|
||||
|
||||
if len(self) == 0:
|
||||
# Skip as there are not logs to send yet
|
||||
return
|
||||
|
||||
request_body = self.to_request()
|
||||
self.root['streams'][0]['values'] = []
|
||||
request_header = {'Content-Type': 'application/json'}
|
||||
|
||||
try:
|
||||
result = requests.post(self.url, headers=request_header, data=request_body, timeout=5)
|
||||
result.raise_for_status()
|
||||
except requests.RequestException:
|
||||
logger.warning('Failed to upload logs to loki')
|
||||
|
||||
|
||||
class Loki_log_handler(logging.Handler):
|
||||
'''
|
||||
A log handler that sends logs to loki.
|
||||
'''
|
||||
|
||||
def __init__(self, url, dry_run):
|
||||
super().__init__()
|
||||
self.buffer = Loki_log_buffer(url, dry_run)
|
||||
|
||||
def emit(self, record):
|
||||
'''
|
||||
Add a log record from the logging module to the stream.
|
||||
'''
|
||||
self.raw(record.getMessage())
|
||||
|
||||
def add_label(self, key, value):
|
||||
'''
|
||||
Add a label to the logging stream.
|
||||
'''
|
||||
self.buffer.add_label(key, value)
|
||||
|
||||
def raw(self, msg):
|
||||
'''
|
||||
Add an arbitrary string as a log entry to the stream.
|
||||
'''
|
||||
self.buffer.add_value(msg)
|
||||
|
||||
if len(self.buffer) > MAX_BUFFER_LINES:
|
||||
self.buffer.flush()
|
||||
|
||||
def flush(self):
|
||||
'''
|
||||
Send the logs to loki and empty the buffer.
|
||||
'''
|
||||
self.buffer.flush()
|
||||
|
||||
|
||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Add a handler to the root logger to regularly send the logs to loki.
|
||||
'''
|
||||
url = hook_config.get('url')
|
||||
loki = Loki_log_handler(url, dry_run)
|
||||
|
||||
for key, value in hook_config.get('labels').items():
|
||||
if value == '__hostname':
|
||||
loki.add_label(key, platform.node())
|
||||
elif value == '__config':
|
||||
loki.add_label(key, os.path.basename(config_filename))
|
||||
elif value == '__config_path':
|
||||
loki.add_label(key, config_filename)
|
||||
else:
|
||||
loki.add_label(key, value)
|
||||
|
||||
logging.getLogger().addHandler(loki)
|
||||
|
||||
|
||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Add an entry to the loki logger with the current state.
|
||||
'''
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, Loki_log_handler):
|
||||
if state in MONITOR_STATE_TO_LOKI.keys():
|
||||
handler.raw(f'{config_filename}: {MONITOR_STATE_TO_LOKI[state]} backup')
|
||||
|
||||
|
||||
def destroy_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||
'''
|
||||
Remove the monitor handler that was added to the root logger.
|
||||
'''
|
||||
logger = logging.getLogger()
|
||||
|
||||
for handler in tuple(logger.handlers):
|
||||
if isinstance(handler, Loki_log_handler):
|
||||
handler.flush()
|
||||
logger.removeHandler(handler)
|
||||
@@ -0,0 +1,237 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
|
||||
from borgmatic.execute import (
|
||||
execute_command,
|
||||
execute_command_and_capture_output,
|
||||
execute_command_with_processes,
|
||||
)
|
||||
from borgmatic.hooks import dump
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_dump_path(config): # pragma: no cover
|
||||
'''
|
||||
Make the dump path from the given configuration dict and the name of this hook.
|
||||
'''
|
||||
return dump.make_data_source_dump_path(
|
||||
config.get('borgmatic_source_directory'), 'mariadb_databases'
|
||||
)
|
||||
|
||||
|
||||
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||
|
||||
|
||||
def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
|
||||
'''
|
||||
Given a requested database config, return the corresponding sequence of database names to dump.
|
||||
In the case of "all", query for the names of databases on the configured host and return them,
|
||||
excluding any system databases that will cause problems during restore.
|
||||
'''
|
||||
if database['name'] != 'all':
|
||||
return (database['name'],)
|
||||
if dry_run:
|
||||
return ()
|
||||
|
||||
show_command = (
|
||||
('mariadb',)
|
||||
+ (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
||||
+ (('--user', database['username']) if 'username' in database else ())
|
||||
+ ('--skip-column-names', '--batch')
|
||||
+ ('--execute', 'show schemas')
|
||||
)
|
||||
logger.debug(f'{log_prefix}: Querying for "all" MariaDB databases to dump')
|
||||
show_output = execute_command_and_capture_output(
|
||||
show_command, extra_environment=extra_environment
|
||||
)
|
||||
|
||||
return tuple(
|
||||
show_name
|
||||
for show_name in show_output.strip().splitlines()
|
||||
if show_name not in SYSTEM_DATABASE_NAMES
|
||||
)
|
||||
|
||||
|
||||
def execute_dump_command(
|
||||
database, log_prefix, dump_path, database_names, extra_environment, dry_run, dry_run_label
|
||||
):
|
||||
'''
|
||||
Kick off a dump for the given MariaDB database (provided as a configuration dict) to a named
|
||||
pipe constructed from the given dump path and database name. Use the given log prefix in any
|
||||
log entries.
|
||||
|
||||
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
|
||||
this is a dry run, then don't actually dump anything and return None.
|
||||
'''
|
||||
database_name = database['name']
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database['name'], database.get('hostname')
|
||||
)
|
||||
|
||||
if os.path.exists(dump_filename):
|
||||
logger.warning(
|
||||
f'{log_prefix}: Skipping duplicate dump of MariaDB database "{database_name}" to {dump_filename}'
|
||||
)
|
||||
return None
|
||||
|
||||
dump_command = (
|
||||
('mariadb-dump',)
|
||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||
+ (('--add-drop-database',) if database.get('add_drop_database', True) else ())
|
||||
+ (('--host', database['hostname']) if 'hostname' in database else ())
|
||||
+ (('--port', str(database['port'])) if 'port' in database else ())
|
||||
+ (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
|
||||
+ (('--user', database['username']) if 'username' in database else ())
|
||||
+ ('--databases',)
|
||||
+ database_names
|
||||
+ ('--result-file', dump_filename)
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f'{log_prefix}: Dumping MariaDB database "{database_name}" to {dump_filename}{dry_run_label}'
|
||||
)
|
||||
if dry_run:
|
||||
return None
|
||||
|
||||
dump.create_named_pipe_for_dump(dump_filename)
|
||||
|
||||
return execute_command(
|
||||
dump_command,
|
||||
extra_environment=extra_environment,
|
||||
run_to_completion=False,
|
||||
)
|
||||
|
||||
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given MariaDB databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
configuration dict to construct the destination path and the given log prefix in any log
|
||||
entries.
|
||||
|
||||
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
|
||||
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
|
||||
processes = []
|
||||
|
||||
logger.info(f'{log_prefix}: Dumping MariaDB databases{dry_run_label}')
|
||||
|
||||
for database in databases:
|
||||
dump_path = make_dump_path(config)
|
||||
extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
|
||||
dump_database_names = database_names_to_dump(
|
||||
database, extra_environment, log_prefix, dry_run
|
||||
)
|
||||
|
||||
if not dump_database_names:
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
raise ValueError('Cannot find any MariaDB databases to dump.')
|
||||
|
||||
if database['name'] == 'all' and database.get('format'):
|
||||
for dump_name in dump_database_names:
|
||||
renamed_database = copy.copy(database)
|
||||
renamed_database['name'] = dump_name
|
||||
processes.append(
|
||||
execute_dump_command(
|
||||
renamed_database,
|
||||
log_prefix,
|
||||
dump_path,
|
||||
(dump_name,),
|
||||
extra_environment,
|
||||
dry_run,
|
||||
dry_run_label,
|
||||
)
|
||||
)
|
||||
else:
|
||||
processes.append(
|
||||
execute_dump_command(
|
||||
database,
|
||||
log_prefix,
|
||||
dump_path,
|
||||
dump_database_names,
|
||||
extra_environment,
|
||||
dry_run,
|
||||
dry_run_label,
|
||||
)
|
||||
)
|
||||
|
||||
return [process for process in processes if process]
|
||||
|
||||
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', log_prefix, dry_run)
|
||||
|
||||
|
||||
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
'''
|
||||
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
|
||||
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
):
|
||||
'''
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
hostname = connection_params['hostname'] or data_source.get(
|
||||
'restore_hostname', data_source.get('hostname')
|
||||
)
|
||||
port = str(
|
||||
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||
)
|
||||
username = connection_params['username'] or data_source.get(
|
||||
'restore_username', data_source.get('username')
|
||||
)
|
||||
password = connection_params['password'] or data_source.get(
|
||||
'restore_password', data_source.get('password')
|
||||
)
|
||||
|
||||
restore_command = (
|
||||
('mariadb', '--batch')
|
||||
+ (
|
||||
tuple(data_source['restore_options'].split(' '))
|
||||
if 'restore_options' in data_source
|
||||
else ()
|
||||
)
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', str(port)) if port else ())
|
||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||
+ (('--user', username) if username else ())
|
||||
)
|
||||
extra_environment = {'MYSQL_PWD': password} if password else None
|
||||
|
||||
logger.debug(f"{log_prefix}: Restoring MariaDB database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
|
||||
# if the restore paths don't exist in the archive.
|
||||
execute_command_with_processes(
|
||||
restore_command,
|
||||
[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment=extra_environment,
|
||||
)
|
||||
+38
-53
@@ -10,12 +10,12 @@ def make_dump_path(config): # pragma: no cover
|
||||
'''
|
||||
Make the dump path from the given configuration dict and the name of this hook.
|
||||
'''
|
||||
return dump.make_database_dump_path(
|
||||
return dump.make_data_source_dump_path(
|
||||
config.get('borgmatic_source_directory'), 'mongodb_databases'
|
||||
)
|
||||
|
||||
|
||||
def dump_databases(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the configuration
|
||||
@@ -31,7 +31,7 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
processes = []
|
||||
for database in databases:
|
||||
name = database['name']
|
||||
dump_filename = dump.make_database_dump_filename(
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), name, database.get('hostname')
|
||||
)
|
||||
dump_format = database.get('format', 'archive')
|
||||
@@ -59,81 +59,65 @@ def build_dump_command(database, dump_filename, dump_format):
|
||||
Return the mongodump command from a single database configuration.
|
||||
'''
|
||||
all_databases = database['name'] == 'all'
|
||||
command = ['mongodump']
|
||||
if dump_format == 'directory':
|
||||
command.extend(('--out', dump_filename))
|
||||
if 'hostname' in database:
|
||||
command.extend(('--host', database['hostname']))
|
||||
if 'port' in database:
|
||||
command.extend(('--port', str(database['port'])))
|
||||
if 'username' in database:
|
||||
command.extend(('--username', database['username']))
|
||||
if 'password' in database:
|
||||
command.extend(('--password', database['password']))
|
||||
if 'authentication_database' in database:
|
||||
command.extend(('--authenticationDatabase', database['authentication_database']))
|
||||
if not all_databases:
|
||||
command.extend(('--db', database['name']))
|
||||
if 'options' in database:
|
||||
command.extend(database['options'].split(' '))
|
||||
if dump_format != 'directory':
|
||||
command.extend(('--archive', '>', dump_filename))
|
||||
return command
|
||||
|
||||
return (
|
||||
('mongodump',)
|
||||
+ (('--out', dump_filename) if dump_format == 'directory' else ())
|
||||
+ (('--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 ())
|
||||
+ (('--password', database['password']) if 'password' in database else ())
|
||||
+ (
|
||||
('--authenticationDatabase', database['authentication_database'])
|
||||
if 'authentication_database' in database
|
||||
else ()
|
||||
)
|
||||
+ (('--db', database['name']) if not all_databases else ())
|
||||
+ (tuple(database['options'].split(' ')) if 'options' in database else ())
|
||||
+ (('--archive', '>', dump_filename) if dump_format != 'directory' else ())
|
||||
)
|
||||
|
||||
|
||||
def remove_database_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the log
|
||||
prefix in any log entries. Use the given configuration dict to construct the destination path.
|
||||
If this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_database_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
|
||||
|
||||
|
||||
def make_database_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
|
||||
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||
in an archive.
|
||||
'''
|
||||
return dump.make_database_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
|
||||
|
||||
def restore_database_dump(
|
||||
databases_config, config, log_prefix, database_name, dry_run, extract_process, connection_params
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
):
|
||||
'''
|
||||
Restore the given MongoDB database from an extract stream. The databases are supplied as a
|
||||
sequence containing one dict describing each database (as per the configuration schema), but
|
||||
only the database corresponding to the given database name is restored. Use the configuration
|
||||
dict to construct the destination path and the given log prefix in any log entries. If this is a
|
||||
dry run, then don't actually restore anything. Trigger the given active extract process (an
|
||||
instance of subprocess.Popen) to produce output to consume.
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
|
||||
If the extract process is None, then restore the dump from the filesystem rather than from an
|
||||
extract stream.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
|
||||
try:
|
||||
database = next(
|
||||
database_config
|
||||
for database_config in databases_config
|
||||
if database_config.get('name') == database_name
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
f'A database named "{database_name}" could not be found in the configuration'
|
||||
)
|
||||
|
||||
dump_filename = dump.make_database_dump_filename(
|
||||
make_dump_path(config), database['name'], database.get('hostname')
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), data_source['name'], data_source.get('hostname')
|
||||
)
|
||||
restore_command = build_restore_command(
|
||||
extract_process, database, dump_filename, connection_params
|
||||
extract_process, data_source, dump_filename, connection_params
|
||||
)
|
||||
|
||||
logger.debug(f"{log_prefix}: Restoring MongoDB database {database['name']}{dry_run_label}")
|
||||
logger.debug(f"{log_prefix}: Restoring MongoDB database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
@@ -168,7 +152,7 @@ def build_restore_command(extract_process, database, dump_filename, connection_p
|
||||
else:
|
||||
command.extend(('--dir', dump_filename))
|
||||
if database['name'] != 'all':
|
||||
command.extend(('--drop', '--db', database['name']))
|
||||
command.extend(('--drop',))
|
||||
if hostname:
|
||||
command.extend(('--host', hostname))
|
||||
if port:
|
||||
@@ -181,7 +165,8 @@ def build_restore_command(extract_process, database, dump_filename, connection_p
|
||||
command.extend(('--authenticationDatabase', database['authentication_database']))
|
||||
if 'restore_options' in database:
|
||||
command.extend(database['restore_options'].split(' '))
|
||||
if database['schemas']:
|
||||
if database.get('schemas'):
|
||||
for schema in database['schemas']:
|
||||
command.extend(('--nsInclude', schema))
|
||||
|
||||
return command
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
|
||||
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy')
|
||||
MONITOR_HOOK_NAMES = ('apprise', 'healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki')
|
||||
|
||||
|
||||
class State(Enum):
|
||||
|
||||
+33
-36
@@ -16,7 +16,9 @@ def make_dump_path(config): # pragma: no cover
|
||||
'''
|
||||
Make the dump path from the given configuration dict and the name of this hook.
|
||||
'''
|
||||
return dump.make_database_dump_path(config.get('borgmatic_source_directory'), 'mysql_databases')
|
||||
return dump.make_data_source_dump_path(
|
||||
config.get('borgmatic_source_directory'), 'mysql_databases'
|
||||
)
|
||||
|
||||
|
||||
SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
|
||||
@@ -60,16 +62,17 @@ def execute_dump_command(
|
||||
):
|
||||
'''
|
||||
Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
|
||||
named pipe constructed from the given dump path and database names. Use the given log prefix in
|
||||
named pipe constructed from the given dump path and database name. Use the given log prefix in
|
||||
any log entries.
|
||||
|
||||
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
|
||||
this is a dry run, then don't actually dump anything and return None.
|
||||
'''
|
||||
database_name = database['name']
|
||||
dump_filename = dump.make_database_dump_filename(
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database['name'], database.get('hostname')
|
||||
)
|
||||
|
||||
if os.path.exists(dump_filename):
|
||||
logger.warning(
|
||||
f'{log_prefix}: Skipping duplicate dump of MySQL database "{database_name}" to {dump_filename}'
|
||||
@@ -104,7 +107,7 @@ def execute_dump_command(
|
||||
)
|
||||
|
||||
|
||||
def dump_databases(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given MySQL/MariaDB databases to a named pipe. The databases are supplied as a sequence
|
||||
of dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
@@ -162,61 +165,55 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
return [process for process in processes if process]
|
||||
|
||||
|
||||
def remove_database_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_database_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MySQL', log_prefix, dry_run)
|
||||
|
||||
|
||||
def make_database_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
'''
|
||||
return dump.make_database_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
|
||||
|
||||
def restore_database_dump(
|
||||
databases_config, config, log_prefix, database_name, dry_run, extract_process, connection_params
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
):
|
||||
'''
|
||||
Restore the given MySQL/MariaDB database from an extract stream. The databases are supplied as a
|
||||
sequence containing one dict describing each database (as per the configuration schema), but
|
||||
only the database corresponding to the given database name is restored. Use the given log
|
||||
prefix in any log entries. If this is a dry run, then don't actually restore anything. Trigger
|
||||
the given active extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
|
||||
try:
|
||||
database = next(
|
||||
database_config
|
||||
for database_config in databases_config
|
||||
if database_config.get('name') == database_name
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
f'A database named "{database_name}" could not be found in the configuration'
|
||||
)
|
||||
|
||||
hostname = connection_params['hostname'] or database.get(
|
||||
'restore_hostname', database.get('hostname')
|
||||
hostname = connection_params['hostname'] or data_source.get(
|
||||
'restore_hostname', data_source.get('hostname')
|
||||
)
|
||||
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
|
||||
username = connection_params['username'] or database.get(
|
||||
'restore_username', database.get('username')
|
||||
port = str(
|
||||
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||
)
|
||||
password = connection_params['password'] or database.get(
|
||||
'restore_password', database.get('password')
|
||||
username = connection_params['username'] or data_source.get(
|
||||
'restore_username', data_source.get('username')
|
||||
)
|
||||
password = connection_params['password'] or data_source.get(
|
||||
'restore_password', data_source.get('password')
|
||||
)
|
||||
|
||||
restore_command = (
|
||||
('mysql', '--batch')
|
||||
+ (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
|
||||
+ (
|
||||
tuple(data_source['restore_options'].split(' '))
|
||||
if 'restore_options' in data_source
|
||||
else ()
|
||||
)
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', str(port)) if port else ())
|
||||
+ (('--protocol', 'tcp') if hostname or port else ())
|
||||
@@ -224,7 +221,7 @@ def restore_database_dump(
|
||||
)
|
||||
extra_environment = {'MYSQL_PWD': password} if password else None
|
||||
|
||||
logger.debug(f"{log_prefix}: Restoring MySQL database {database['name']}{dry_run_label}")
|
||||
logger.debug(f"{log_prefix}: Restoring MySQL database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
@@ -28,8 +28,8 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||
state_config = hook_config.get(
|
||||
state.name.lower(),
|
||||
{
|
||||
'title': f'A Borgmatic {state.name} event happened',
|
||||
'message': f'A Borgmatic {state.name} event happened',
|
||||
'title': f'A borgmatic {state.name} event happened',
|
||||
'message': f'A borgmatic {state.name} event happened',
|
||||
'priority': 'default',
|
||||
'tags': 'borgmatic',
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ def make_dump_path(config): # pragma: no cover
|
||||
'''
|
||||
Make the dump path from the given configuration dict and the name of this hook.
|
||||
'''
|
||||
return dump.make_database_dump_path(
|
||||
return dump.make_data_source_dump_path(
|
||||
config.get('borgmatic_source_directory'), 'postgresql_databases'
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
|
||||
)
|
||||
|
||||
|
||||
def dump_databases(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given PostgreSQL databases to a named pipe. The databases are supplied as a sequence of
|
||||
dicts, one dict describing each database as per the configuration schema. Use the given
|
||||
@@ -126,7 +126,7 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
dump_format = database.get('format', None if database_name == 'all' else 'custom')
|
||||
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
|
||||
dump_command = database.get('pg_dump_command') or default_dump_command
|
||||
dump_filename = dump.make_database_dump_filename(
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
dump_path, database_name, database.get('hostname')
|
||||
)
|
||||
if os.path.exists(dump_filename):
|
||||
@@ -183,34 +183,33 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
return processes
|
||||
|
||||
|
||||
def remove_database_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
'''
|
||||
Remove all database dump files for this hook regardless of the given databases. Use the given
|
||||
configuration dict to construct the destination path and the log prefix in any log entries. If
|
||||
this is a dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_database_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'PostgreSQL', log_prefix, dry_run)
|
||||
|
||||
|
||||
def make_database_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
'''
|
||||
Given a sequence of configurations dicts, a configuration dict, a prefix to log with, and a
|
||||
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||
archive.
|
||||
'''
|
||||
return dump.make_database_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
return dump.make_data_source_dump_filename(make_dump_path(config), name, hostname='*')
|
||||
|
||||
|
||||
def restore_database_dump(
|
||||
databases_config, config, log_prefix, database_name, dry_run, extract_process, connection_params
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
):
|
||||
'''
|
||||
Restore the given PostgreSQL database from an extract stream. The databases are supplied as a
|
||||
sequence containing one dict describing each database (as per the configuration schema), but
|
||||
only the database corresponding to the given database name is restored. Use the given
|
||||
configuration dict to construct the destination path and the given log prefix in any log
|
||||
entries. If this is a dry run, then don't actually restore anything. Trigger the given active
|
||||
extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
|
||||
If the extract process is None, then restore the dump from the filesystem rather than from an
|
||||
extract stream.
|
||||
@@ -219,66 +218,66 @@ def restore_database_dump(
|
||||
hostname, port, username, and password.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
|
||||
try:
|
||||
database = next(
|
||||
database_config
|
||||
for database_config in databases_config
|
||||
if database_config.get('name') == database_name
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
f'A database named "{database_name}" could not be found in the configuration'
|
||||
)
|
||||
|
||||
hostname = connection_params['hostname'] or database.get(
|
||||
'restore_hostname', database.get('hostname')
|
||||
hostname = connection_params['hostname'] or data_source.get(
|
||||
'restore_hostname', data_source.get('hostname')
|
||||
)
|
||||
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
|
||||
username = connection_params['username'] or database.get(
|
||||
'restore_username', database.get('username')
|
||||
port = str(
|
||||
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||
)
|
||||
username = connection_params['username'] or data_source.get(
|
||||
'restore_username', data_source.get('username')
|
||||
)
|
||||
|
||||
all_databases = bool(database['name'] == 'all')
|
||||
dump_filename = dump.make_database_dump_filename(
|
||||
make_dump_path(config), database['name'], database.get('hostname')
|
||||
all_databases = bool(data_source['name'] == 'all')
|
||||
dump_filename = dump.make_data_source_dump_filename(
|
||||
make_dump_path(config), data_source['name'], data_source.get('hostname')
|
||||
)
|
||||
psql_command = shlex.split(database.get('psql_command') or 'psql')
|
||||
psql_command = shlex.split(data_source.get('psql_command') or 'psql')
|
||||
analyze_command = (
|
||||
tuple(psql_command)
|
||||
+ ('--no-password', '--no-psqlrc', '--quiet')
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', port) if port else ())
|
||||
+ (('--username', username) if username else ())
|
||||
+ (('--dbname', database['name']) if not all_databases else ())
|
||||
+ (tuple(database['analyze_options'].split(' ')) if 'analyze_options' in database else ())
|
||||
+ (('--dbname', data_source['name']) if not all_databases else ())
|
||||
+ (
|
||||
tuple(data_source['analyze_options'].split(' '))
|
||||
if 'analyze_options' in data_source
|
||||
else ()
|
||||
)
|
||||
+ ('--command', 'ANALYZE')
|
||||
)
|
||||
use_psql_command = all_databases or database.get('format') == 'plain'
|
||||
pg_restore_command = shlex.split(database.get('pg_restore_command') or 'pg_restore')
|
||||
use_psql_command = all_databases or data_source.get('format') == 'plain'
|
||||
pg_restore_command = shlex.split(data_source.get('pg_restore_command') or 'pg_restore')
|
||||
restore_command = (
|
||||
tuple(psql_command if use_psql_command else pg_restore_command)
|
||||
+ ('--no-password',)
|
||||
+ (('--no-psqlrc',) if use_psql_command else ('--if-exists', '--exit-on-error', '--clean'))
|
||||
+ (('--dbname', database['name']) if not all_databases else ())
|
||||
+ (('--dbname', data_source['name']) if not all_databases else ())
|
||||
+ (('--host', hostname) if hostname else ())
|
||||
+ (('--port', port) if port else ())
|
||||
+ (('--username', username) if username else ())
|
||||
+ (('--no-owner',) if database.get('no_owner', False) else ())
|
||||
+ (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
|
||||
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
|
||||
+ (
|
||||
tuple(data_source['restore_options'].split(' '))
|
||||
if 'restore_options' in data_source
|
||||
else ()
|
||||
)
|
||||
+ (() if extract_process else (dump_filename,))
|
||||
+ tuple(
|
||||
itertools.chain.from_iterable(('--schema', schema) for schema in database['schemas'])
|
||||
if database.get('schemas')
|
||||
itertools.chain.from_iterable(('--schema', schema) for schema in data_source['schemas'])
|
||||
if data_source.get('schemas')
|
||||
else ()
|
||||
)
|
||||
)
|
||||
|
||||
extra_environment = make_extra_environment(
|
||||
database, restore_connection_params=connection_params
|
||||
data_source, restore_connection_params=connection_params
|
||||
)
|
||||
|
||||
logger.debug(f"{log_prefix}: Restoring PostgreSQL database {database['name']}{dry_run_label}")
|
||||
logger.debug(
|
||||
f"{log_prefix}: Restoring PostgreSQL database {data_source['name']}{dry_run_label}"
|
||||
)
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
+18
-30
@@ -11,12 +11,12 @@ def make_dump_path(config): # pragma: no cover
|
||||
'''
|
||||
Make the dump path from the given configuration dict and the name of this hook.
|
||||
'''
|
||||
return dump.make_database_dump_path(
|
||||
return dump.make_data_source_dump_path(
|
||||
config.get('borgmatic_source_directory'), 'sqlite_databases'
|
||||
)
|
||||
|
||||
|
||||
def dump_databases(databases, config, log_prefix, dry_run):
|
||||
def dump_data_sources(databases, config, log_prefix, dry_run):
|
||||
'''
|
||||
Dump the given SQLite3 databases to a file. The databases are supplied as a sequence of
|
||||
configuration dicts, as per the configuration schema. Use the given configuration dict to
|
||||
@@ -32,14 +32,14 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
database_path = database['path']
|
||||
|
||||
if database['name'] == 'all':
|
||||
logger.warning('The "all" database name has no meaning for SQLite3 databases')
|
||||
logger.warning('The "all" database name has no meaning for SQLite databases')
|
||||
if not os.path.exists(database_path):
|
||||
logger.warning(
|
||||
f'{log_prefix}: No SQLite database at {database_path}; An empty database will be created and dumped'
|
||||
f'{log_prefix}: No SQLite database at {database_path}; an empty database will be created and dumped'
|
||||
)
|
||||
|
||||
dump_path = make_dump_path(config)
|
||||
dump_filename = dump.make_database_dump_filename(dump_path, database['name'])
|
||||
dump_filename = dump.make_data_source_dump_filename(dump_path, database['name'])
|
||||
if os.path.exists(dump_filename):
|
||||
logger.warning(
|
||||
f'{log_prefix}: Skipping duplicate dump of SQLite database at {database_path} to {dump_filename}'
|
||||
@@ -65,49 +65,37 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
return processes
|
||||
|
||||
|
||||
def remove_database_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
def remove_data_source_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
|
||||
'''
|
||||
Remove the given SQLite3 database dumps from the filesystem. The databases are supplied as a
|
||||
sequence of configuration dicts, as per the configuration schema. Use the given configuration
|
||||
dict to construct the destination path and the given log prefix in any log entries. If this is a
|
||||
dry run, then don't actually remove anything.
|
||||
'''
|
||||
dump.remove_database_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'SQLite', log_prefix, dry_run)
|
||||
|
||||
|
||||
def make_database_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
def make_data_source_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
|
||||
'''
|
||||
Make a pattern that matches the given SQLite3 databases. The databases are supplied as a
|
||||
sequence of configuration dicts, as per the configuration schema.
|
||||
'''
|
||||
return dump.make_database_dump_filename(make_dump_path(config), name)
|
||||
return dump.make_data_source_dump_filename(make_dump_path(config), name)
|
||||
|
||||
|
||||
def restore_database_dump(
|
||||
databases_config, config, log_prefix, database_name, dry_run, extract_process, connection_params
|
||||
def restore_data_source_dump(
|
||||
hook_config, config, log_prefix, data_source, dry_run, extract_process, connection_params
|
||||
):
|
||||
'''
|
||||
Restore the given SQLite3 database from an extract stream. The databases are supplied as a
|
||||
sequence containing one dict describing each database (as per the configuration schema), but
|
||||
only the database corresponding to the given database name is restored. Use the given log prefix
|
||||
in any log entries. If this is a dry run, then don't actually restore anything. Trigger the
|
||||
given active extract process (an instance of subprocess.Popen) to produce output to consume.
|
||||
Restore a database from the given extract stream. The database is supplied as a data source
|
||||
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||
process (an instance of subprocess.Popen) to produce output to consume.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
|
||||
try:
|
||||
database = next(
|
||||
database_config
|
||||
for database_config in databases_config
|
||||
if database_config.get('name') == database_name
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(
|
||||
f'A database named "{database_name}" could not be found in the configuration'
|
||||
)
|
||||
|
||||
database_path = connection_params['restore_path'] or database.get(
|
||||
'restore_path', database.get('path')
|
||||
database_path = connection_params['restore_path'] or data_source.get(
|
||||
'restore_path', data_source.get('path')
|
||||
)
|
||||
|
||||
logger.debug(f'{log_prefix}: Restoring SQLite database at {database_path}{dry_run_label}')
|
||||
|
||||
+19
-17
@@ -166,15 +166,15 @@ def configure_logging(
|
||||
|
||||
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
|
||||
'''
|
||||
add_custom_log_levels()
|
||||
|
||||
if syslog_log_level is None:
|
||||
syslog_log_level = console_log_level
|
||||
syslog_log_level = logging.DISABLED
|
||||
if log_file_log_level is None:
|
||||
log_file_log_level = console_log_level
|
||||
if monitoring_log_level is None:
|
||||
monitoring_log_level = console_log_level
|
||||
|
||||
add_custom_log_levels()
|
||||
|
||||
# Log certain log levels to console stderr and others to stdout. This supports use cases like
|
||||
# grepping (non-error) output.
|
||||
console_disabled = logging.NullHandler()
|
||||
@@ -194,8 +194,11 @@ def configure_logging(
|
||||
console_handler.setFormatter(Console_color_formatter())
|
||||
console_handler.setLevel(console_log_level)
|
||||
|
||||
syslog_path = None
|
||||
if log_file is None and syslog_log_level != logging.DISABLED:
|
||||
handlers = [console_handler]
|
||||
|
||||
if syslog_log_level != logging.DISABLED:
|
||||
syslog_path = None
|
||||
|
||||
if os.path.exists('/dev/log'):
|
||||
syslog_path = '/dev/log'
|
||||
elif os.path.exists('/var/run/syslog'):
|
||||
@@ -203,14 +206,15 @@ def configure_logging(
|
||||
elif os.path.exists('/var/run/log'):
|
||||
syslog_path = '/var/run/log'
|
||||
|
||||
if syslog_path and not interactive_console():
|
||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||
syslog_handler.setFormatter(
|
||||
logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
|
||||
)
|
||||
syslog_handler.setLevel(syslog_log_level)
|
||||
handlers = (console_handler, syslog_handler)
|
||||
elif log_file and log_file_log_level != logging.DISABLED:
|
||||
if syslog_path:
|
||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||
syslog_handler.setFormatter(
|
||||
logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
|
||||
)
|
||||
syslog_handler.setLevel(syslog_log_level)
|
||||
handlers.append(syslog_handler)
|
||||
|
||||
if log_file and log_file_log_level != logging.DISABLED:
|
||||
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter(
|
||||
@@ -218,11 +222,9 @@ def configure_logging(
|
||||
)
|
||||
)
|
||||
file_handler.setLevel(log_file_log_level)
|
||||
handlers = (console_handler, file_handler)
|
||||
else:
|
||||
handlers = (console_handler,)
|
||||
handlers.append(file_handler)
|
||||
|
||||
logging.basicConfig(
|
||||
level=min(console_log_level, syslog_log_level, log_file_log_level, monitoring_log_level),
|
||||
level=min(handler.level for handler in handlers),
|
||||
handlers=handlers,
|
||||
)
|
||||
|
||||
+10
-2
@@ -23,12 +23,20 @@ def handle_signal(signal_number, frame):
|
||||
if signal_number == signal.SIGTERM:
|
||||
logger.critical('Exiting due to TERM signal')
|
||||
sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
|
||||
elif signal_number == signal.SIGINT:
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
def configure_signals():
|
||||
'''
|
||||
Configure borgmatic's signal handlers to pass relevant signals through to any child processes
|
||||
like Borg. Note that SIGINT gets passed through even without these changes.
|
||||
like Borg.
|
||||
'''
|
||||
for signal_number in (signal.SIGHUP, signal.SIGTERM, signal.SIGUSR1, signal.SIGUSR2):
|
||||
for signal_number in (
|
||||
signal.SIGHUP,
|
||||
signal.SIGINT,
|
||||
signal.SIGTERM,
|
||||
signal.SIGUSR1,
|
||||
signal.SIGUSR2,
|
||||
):
|
||||
signal.signal(signal_number, handle_signal)
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ each.
|
||||
If you find a security vulnerability, please [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) or [send email
|
||||
directly](mailto:witten@torsion.org) as appropriate. You should expect to hear
|
||||
back within a few days at most, and generally sooner.
|
||||
back within a few days at most and generally sooner.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
font-size: 1rem; /* Reset */
|
||||
}
|
||||
.elv-toc details {
|
||||
--details-force-closed: (max-width: 63.9375em); /* 1023px */
|
||||
--details-force-closed: (max-width: 79.9375em); /* 1023px */
|
||||
}
|
||||
.elv-toc details > summary {
|
||||
font-size: 1.375rem; /* 22px /16 */
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
@media (min-width: 64em) { /* 1024px */
|
||||
@media (min-width: 80em) {
|
||||
.elv-toc {
|
||||
position: absolute;
|
||||
left: 3rem;
|
||||
|
||||
@@ -121,7 +121,7 @@ main h1:first-child,
|
||||
main .elv-toc + h1 {
|
||||
border-bottom: 2px dotted #666;
|
||||
}
|
||||
@media (min-width: 64em) { /* 1024px */
|
||||
@media (min-width: 80em) {
|
||||
main .elv-toc + h1,
|
||||
main .elv-toc + h2 {
|
||||
margin-top: 0;
|
||||
@@ -243,10 +243,10 @@ footer.elv-layout {
|
||||
.elv-layout-full {
|
||||
max-width: none;
|
||||
}
|
||||
@media (min-width: 64em) { /* 1024px */
|
||||
@media (min-width: 80em) {
|
||||
.elv-layout-toc {
|
||||
padding-left: 15rem;
|
||||
max-width: 60rem;
|
||||
max-width: 76rem;
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
running backups and specify `after_backup` hooks to perform cleanup steps
|
||||
afterwards. Here's an example:
|
||||
|
||||
```yaml
|
||||
@@ -126,7 +126,7 @@ 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
|
||||
(when enabled). For more information, read about <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/inspect-your-backups/">inspecting
|
||||
your backups</a>.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ consistent snapshot that is more suited for backups.
|
||||
|
||||
Fortunately, borgmatic includes built-in support for creating database dumps
|
||||
prior to running backups. For example, here is everything you need to dump and
|
||||
backup a couple of local PostgreSQL databases and a MySQL/MariaDB database.
|
||||
backup a couple of local PostgreSQL databases and a MySQL database.
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
@@ -46,6 +46,16 @@ sqlite_databases:
|
||||
path: /var/lib/sqlite3/mydb.sqlite
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.2</span> If you're
|
||||
using MariaDB, use the MariaDB database hook instead of `mysql_databases:` as
|
||||
the MariaDB hook calls native MariaDB commands instead of the deprecated MySQL
|
||||
ones. For instance:
|
||||
|
||||
```yaml
|
||||
mariadb_databases:
|
||||
- name: comments
|
||||
```
|
||||
|
||||
As part of each backup, borgmatic streams a database dump for each configured
|
||||
database directly to Borg, so it's included in the backup without consuming
|
||||
additional disk space. (The exceptions are the PostgreSQL/MongoDB "directory"
|
||||
@@ -75,16 +85,23 @@ postgresql_databases:
|
||||
password: trustsome1
|
||||
format: tar
|
||||
options: "--role=someone"
|
||||
mariadb_databases:
|
||||
- name: photos
|
||||
hostname: database3.example.org
|
||||
port: 3307
|
||||
username: root
|
||||
password: trustsome1
|
||||
options: "--skip-comments"
|
||||
mysql_databases:
|
||||
- name: posts
|
||||
hostname: database3.example.org
|
||||
hostname: database4.example.org
|
||||
port: 3307
|
||||
username: root
|
||||
password: trustsome1
|
||||
options: "--skip-comments"
|
||||
mongodb_databases:
|
||||
- name: messages
|
||||
hostname: database4.example.org
|
||||
hostname: database5.example.org
|
||||
port: 27018
|
||||
username: dbuser
|
||||
password: trustsome1
|
||||
@@ -108,6 +125,8 @@ If you want to dump all databases on a host, use `all` for the database name:
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: all
|
||||
mariadb_databases:
|
||||
- name: all
|
||||
mysql_databases:
|
||||
- name: all
|
||||
mongodb_databases:
|
||||
@@ -123,15 +142,18 @@ The SQLite hook in particular does not consider "all" a special database name.
|
||||
these options in the `hooks:` section of your configuration.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.6</span> With
|
||||
PostgreSQL and MySQL, you can optionally dump "all" databases to separate
|
||||
files instead of one combined dump file, allowing more convenient restores of
|
||||
individual databases. Enable this by specifying your desired database dump
|
||||
`format`:
|
||||
PostgreSQL, MariaDB, and MySQL, you can optionally dump "all" databases to
|
||||
separate files instead of one combined dump file, allowing more convenient
|
||||
restores of individual databases. Enable this by specifying your desired
|
||||
database dump `format`:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: all
|
||||
format: custom
|
||||
mariadb_databases:
|
||||
- name: all
|
||||
format: sql
|
||||
mysql_databases:
|
||||
- name: all
|
||||
format: sql
|
||||
@@ -222,10 +244,16 @@ to prepare for this situation, it's a good idea to include borgmatic's own
|
||||
configuration files as part of your regular backups. That way, you can always
|
||||
bring back any missing configuration files in order to restore a database.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.15</span> borgmatic
|
||||
automatically includes configuration files in your backup. See [the
|
||||
documentation on the `config bootstrap`
|
||||
action](https://torsion.org/borgmatic/docs/how-to/extract-a-backup/#extract-the-configuration-files-used-to-create-an-archive)
|
||||
for more information.
|
||||
|
||||
|
||||
## Supported databases
|
||||
|
||||
As of now, borgmatic supports PostgreSQL, MySQL/MariaDB, MongoDB, and SQLite
|
||||
As of now, borgmatic supports PostgreSQL, MariaDB, MySQL, MongoDB, and SQLite
|
||||
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
|
||||
@@ -282,7 +310,8 @@ problem: the `restore` action figures out which repository to use.
|
||||
|
||||
But if you have multiple repositories configured, then you'll need to specify
|
||||
the repository to use via the `--repository` flag. This can be done either
|
||||
with the repository's path or its label as configured in your borgmatic configuration file.
|
||||
with the repository's path or its label as configured in your borgmatic
|
||||
configuration file.
|
||||
|
||||
```bash
|
||||
borgmatic restore --repository repo.borg --archive host-2023-...
|
||||
@@ -338,6 +367,28 @@ within the database dump:
|
||||
borgmatic restore --archive latest --database users --schema tentant1
|
||||
```
|
||||
|
||||
### Restore to an alternate host
|
||||
<span class="minilink minilink-addedin">New in version 1.7.15</span>
|
||||
A database dump can be restored to a host other than the one from which it was
|
||||
originally dumped. The connection parameters like the username, password, and
|
||||
port can also be changed. This can be done from the command line:
|
||||
|
||||
```bash
|
||||
borgmatic restore --archive latest --database users --hostname database2.example.org --port 5433 --username postgres --password trustsome1
|
||||
```
|
||||
|
||||
Or from the configuration file:
|
||||
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
hostname: database1.example.org
|
||||
restore_hostname: database1.example.org
|
||||
restore_port: 5433
|
||||
restore_username: postgres
|
||||
restore_password: trustsome1
|
||||
```
|
||||
|
||||
|
||||
### Limitations
|
||||
|
||||
@@ -398,9 +449,9 @@ dumps with any database system.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### PostgreSQL/MySQL authentication errors
|
||||
### Authentication errors
|
||||
|
||||
With PostgreSQL and MySQL/MariaDB, if you're getting authentication errors
|
||||
With PostgreSQL, MariaDB, and MySQL, if you're getting authentication errors
|
||||
when borgmatic tries to connect to your database, a natural reaction is to
|
||||
increase your borgmatic verbosity with `--verbosity 2` and go looking in the
|
||||
logs. You'll notice though that your database password does not show up in the
|
||||
@@ -414,23 +465,24 @@ authenticated. For instance, with PostgreSQL, check your
|
||||
[pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html)
|
||||
file for that configuration.
|
||||
|
||||
Additionally, MySQL/MariaDB may be picking up some of your credentials from a
|
||||
defaults file like `~/.my.cnf`. If that's the case, then it's possible
|
||||
MySQL/MariaDB ends up using, say, a username from borgmatic's configuration
|
||||
and a password from `~/.my.cnf`. This may result in authentication errors if
|
||||
this combination of credentials is not what you intend.
|
||||
Additionally, MariaDB or MySQL may be picking up some of your credentials from
|
||||
a defaults file like `~/mariadb.cnf` or `~/.my.cnf`. If that's the case, then
|
||||
it's possible MariaDB or MySQL end up using, say, a username from borgmatic's
|
||||
configuration and a password from `~/mariadb.cnf` or `~/.my.cnf`. This may
|
||||
result in authentication errors if this combination of credentials is not what
|
||||
you intend.
|
||||
|
||||
|
||||
### MySQL table lock errors
|
||||
### MariaDB or MySQL table lock errors
|
||||
|
||||
If you encounter table lock errors during a database dump with MySQL/MariaDB,
|
||||
you may need to [use a
|
||||
transaction](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html#option_mysqldump_single-transaction).
|
||||
If you encounter table lock errors during a database dump with MariaDB or
|
||||
MySQL, you may need to [use a
|
||||
transaction](https://mariadb.com/docs/skysql-dbaas/ref/mdb/cli/mariadb-dump/single-transaction/).
|
||||
You can add any additional flags to the `options:` in your database
|
||||
configuration. Here's an example:
|
||||
configuration. Here's an example with MariaDB:
|
||||
|
||||
```yaml
|
||||
mysql_databases:
|
||||
mariadb_databases:
|
||||
- name: posts
|
||||
options: "--single-transaction --quick"
|
||||
```
|
||||
|
||||
@@ -51,6 +51,11 @@ cron job), while only running expensive consistency checks with `check` on a
|
||||
much less frequent basis (e.g. with `borgmatic check` called from a separate
|
||||
cron job).
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.5</span> Instead of
|
||||
(or in addition to) specifying actions on the command-line, you can configure
|
||||
borgmatic to [skip particular
|
||||
actions](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#skipping-actions).
|
||||
|
||||
|
||||
### Consistency check configuration
|
||||
|
||||
@@ -116,8 +121,17 @@ this option in the `consistency:` section of your configuration.
|
||||
|
||||
This tells borgmatic to run the `repository` consistency check at most once
|
||||
every two weeks for a given repository and the `archives` check at most once a
|
||||
month. The `frequency` value is a number followed by a unit of time, e.g. "3
|
||||
days", "1 week", "2 months", etc.
|
||||
month. The `frequency` value is a number followed by a unit of time, e.g. `3
|
||||
days`, `1 week`, `2 months`, etc. The set of possible time units is as
|
||||
follows (singular or plural):
|
||||
|
||||
* `second`
|
||||
* `minute`
|
||||
* `hour`
|
||||
* `day`
|
||||
* `week` (7 days)
|
||||
* `month` (30 days)
|
||||
* `year` (365 days)
|
||||
|
||||
The `frequency` defaults to `always` for a check configured without a
|
||||
`frequency`, which means run this check every time checks run. But if you omit
|
||||
@@ -162,7 +176,16 @@ location:
|
||||
If that's still too slow, you can disable consistency checks entirely,
|
||||
either for a single repository or for all repositories.
|
||||
|
||||
Disabling all consistency checks looks like this:
|
||||
<span class="minilink minilink-addedin">New in version 1.8.5</span> Disabling
|
||||
all consistency checks looks like this:
|
||||
|
||||
```yaml
|
||||
skip_actions:
|
||||
- check
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.5</span> Use this
|
||||
configuration instead:
|
||||
|
||||
```yaml
|
||||
checks:
|
||||
@@ -170,10 +193,10 @@ checks:
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `consistency:` section of your configuration.
|
||||
`checks:` in the `consistency:` section of your configuration.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.6.2</span> `checks`
|
||||
was a plain list of strings without the `name:` part. For instance:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.6.2</span>
|
||||
`checks:` was a plain list of strings without the `name:` part. For instance:
|
||||
|
||||
```yaml
|
||||
checks:
|
||||
@@ -189,7 +212,7 @@ check_repositories:
|
||||
- path/of/repository_to_check.borg
|
||||
```
|
||||
|
||||
Finally, you can override your configuration file's consistency checks, and
|
||||
Finally, you can override your configuration file's consistency checks and
|
||||
run particular checks via the command-line. For instance:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -7,7 +7,12 @@ eleventyNavigation:
|
||||
---
|
||||
## Source code
|
||||
|
||||
To get set up to develop on borgmatic, first clone it via HTTPS or SSH:
|
||||
To get set up to develop on borgmatic, first [`install
|
||||
pipx`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation)
|
||||
to make managing your borgmatic environment easy without impacting other
|
||||
Python applications on your system.
|
||||
|
||||
Then, clone borgmatic via HTTPS or SSH:
|
||||
|
||||
```bash
|
||||
git clone https://projects.torsion.org/borgmatic-collective/borgmatic.git
|
||||
@@ -19,36 +24,34 @@ Or:
|
||||
git clone ssh://git@projects.torsion.org:3022/borgmatic-collective/borgmatic.git
|
||||
```
|
||||
|
||||
Then, install borgmatic
|
||||
"[editable](https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs)"
|
||||
Finally, install borgmatic
|
||||
"[editable](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs)"
|
||||
so that you can run borgmatic actions during development to make sure your
|
||||
changes work.
|
||||
changes work:
|
||||
|
||||
```bash
|
||||
cd borgmatic
|
||||
pip3 install --user --editable .
|
||||
pipx ensurepath
|
||||
pipx install --editable .
|
||||
```
|
||||
|
||||
Note that this will typically install the borgmatic commands into
|
||||
`~/.local/bin`, which may or may not be on your PATH. There are other ways to
|
||||
install borgmatic editable as well, for instance into the system Python
|
||||
install (so without `--user`, as root), or even into a
|
||||
[virtualenv](https://virtualenv.pypa.io/en/stable/). How or where you install
|
||||
borgmatic is up to you, but generally an editable install makes development
|
||||
and testing easier.
|
||||
To get oriented with the borgmatic source code, have a look at the [source
|
||||
code reference](https://torsion.org/borgmatic/docs/reference/source-code/).
|
||||
|
||||
|
||||
## Automated tests
|
||||
|
||||
Assuming you've cloned the borgmatic source code as described above, and
|
||||
you're in the `borgmatic/` working copy, install tox, which is used for
|
||||
setting up testing environments:
|
||||
Assuming you've cloned the borgmatic source code as described above and you're
|
||||
in the `borgmatic/` working copy, install tox, which is used for setting up
|
||||
testing environments. You can either install a system package of tox (likely
|
||||
called `tox` or `python-tox`) or you can install tox with pipx:
|
||||
|
||||
```bash
|
||||
pip3 install --user tox
|
||||
pipx install tox
|
||||
```
|
||||
|
||||
Finally, to actually run tests, run:
|
||||
Finally, to actually run tests, run tox from inside the borgmatic
|
||||
sourcedirectory:
|
||||
|
||||
```bash
|
||||
tox
|
||||
@@ -149,8 +152,8 @@ See the Black, Flake8, and isort documentation for more information.
|
||||
|
||||
Each pull request triggers a continuous integration build which runs the test
|
||||
suite. You can view these builds on
|
||||
[build.torsion.org](https://build.torsion.org/borgmatic-collective/borgmatic), and they're
|
||||
also linked from the commits list on each pull request.
|
||||
[build.torsion.org](https://build.torsion.org/borgmatic-collective/borgmatic),
|
||||
and they're also linked from the commits list on each pull request.
|
||||
|
||||
## Documentation development
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ entire contents of the archive to the current directory, so make sure you're
|
||||
in the right place before running the command—or see below about the
|
||||
`--destination` flag.
|
||||
|
||||
|
||||
## Repository selection
|
||||
|
||||
If you have a single repository in your borgmatic configuration file(s), no
|
||||
@@ -145,3 +144,47 @@ When you're all done exploring your files, unmount your mount point. No
|
||||
```bash
|
||||
borgmatic umount --mount-point /mnt
|
||||
```
|
||||
|
||||
## Extract the configuration files used to create an archive
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.7.15</span> borgmatic
|
||||
automatically stores all the configuration files used to create an archive
|
||||
inside the archive itself. They are stored in the archive using their full
|
||||
paths from the machine being backed up. This is useful in cases where you've
|
||||
lost a configuration file or you want to see what configurations were used to
|
||||
create a particular archive.
|
||||
|
||||
To extract the configuration files from an archive, use the `config bootstrap`
|
||||
action. For example:
|
||||
|
||||
```bash
|
||||
borgmatic config bootstrap --repository repo.borg --destination /tmp
|
||||
```
|
||||
|
||||
This extracts the configuration file from the latest archive in the repository
|
||||
`repo.borg` to `/tmp/etc/borgmatic/config.yaml`, assuming that the only
|
||||
configuration file used to create this archive was located at
|
||||
`/etc/borgmatic/config.yaml` when the archive was created.
|
||||
|
||||
Note that to run the `config bootstrap` action, you don't need to have a
|
||||
borgmatic configuration file. You only need to specify the repository to use via
|
||||
the `--repository` flag; borgmatic will figure out the rest.
|
||||
|
||||
If a destination directory is not specified, the configuration files will be
|
||||
extracted to their original locations, silently *overwriting* any configuration
|
||||
files that may already exist. For example, if a configuration file was located
|
||||
at `/etc/borgmatic/config.yaml` when the archive was created, it will be
|
||||
extracted to `/etc/borgmatic/config.yaml` too.
|
||||
|
||||
If you want to extract the configuration file from a specific archive, use the
|
||||
`--archive` flag:
|
||||
|
||||
```bash
|
||||
borgmatic config bootstrap --repository repo.borg --archive host-2023-01-02T04:06:07.080910 --destination /tmp
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.1</span> Set the
|
||||
`store_config_files` option to `false` to disable the automatic backup of
|
||||
borgmatic configuration files, for instance if they contain sensitive
|
||||
information you don't want to store even inside your encrypted backups. If you
|
||||
do this though, the `config bootstrap` action will no longer work.
|
||||
|
||||
@@ -116,27 +116,30 @@ archive, complete with file sizes.
|
||||
|
||||
## Logging
|
||||
|
||||
By default, borgmatic logs to a local syslog-compatible daemon if one is
|
||||
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
|
||||
similar.
|
||||
|
||||
You can customize the log level used for syslog logging with the
|
||||
`--syslog-verbosity` flag, and this is independent from the console logging
|
||||
`--verbosity` flag described above. For instance, to get additional
|
||||
information about the progress of the backup as it proceeds:
|
||||
By default, borgmatic logs to the console. You can enable simultaneous syslog
|
||||
logging and customize its log level with the `--syslog-verbosity` flag, which
|
||||
is independent from the console logging `--verbosity` flag described above.
|
||||
For instance, to enable syslog logging, run:
|
||||
|
||||
```bash
|
||||
borgmatic --syslog-verbosity 1
|
||||
```
|
||||
|
||||
Or to increase syslog logging to include debug spew:
|
||||
To increase syslog logging further to include debugging information, run:
|
||||
|
||||
```bash
|
||||
borgmatic --syslog-verbosity 2
|
||||
```
|
||||
|
||||
See above for further details about the verbosity levels.
|
||||
|
||||
Where these logs show up depends on your particular system. If you're using
|
||||
systemd, try running `journalctl -xe`. Otherwise, try viewing
|
||||
`/var/log/syslog` or similar.
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.3</span>borgmatic
|
||||
logged to syslog by default whenever run at a non-interactive console.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
If you are using rsyslog or systemd's journal, be aware that by default they
|
||||
@@ -165,7 +168,7 @@ Note that if you use the `--log-file` flag, you are responsible for rotating
|
||||
the log file so it doesn't grow too large, for example with
|
||||
[logrotate](https://wiki.archlinux.org/index.php/Logrotate).
|
||||
|
||||
You can the `--log-file-verbosity` flag to customize the log file's log level:
|
||||
You can use the `--log-file-verbosity` flag to customize the log file's log level:
|
||||
|
||||
```bash
|
||||
borgmatic --log-file /path/to/file.log --log-file-verbosity 2
|
||||
@@ -197,5 +200,5 @@ See the [Python logging
|
||||
documentation](https://docs.python.org/3/library/logging.html#logrecord-attributes)
|
||||
for additional placeholders.
|
||||
|
||||
Note that this `--log-file-format` flg only applies to the specified
|
||||
Note that this `--log-file-format` flag only applies to the specified
|
||||
`--log-file` and not to syslog or other logging.
|
||||
|
||||
@@ -151,9 +151,9 @@ in newer versions of borgmatic.
|
||||
## Configuration includes
|
||||
|
||||
Once you have multiple different configuration files, you might want to share
|
||||
common configuration options across these files with having to copy and paste
|
||||
common configuration options across these files without having to copy and paste
|
||||
them. To achieve this, you can put fragments of common configuration options
|
||||
into a file, and then include or inline that file into one or more borgmatic
|
||||
into a file and then include or inline that file into one or more borgmatic
|
||||
configuration files.
|
||||
|
||||
Let's say that you want to include common consistency check configuration across all
|
||||
@@ -233,18 +233,57 @@ checks:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> These
|
||||
options were organized into sections like `retention:` and `consistency:`.
|
||||
|
||||
Once this include gets merged in, the resulting configuration would have all
|
||||
of the options from the original configuration file *and* the options from the
|
||||
Once this include gets merged in, the resulting configuration has all of the
|
||||
options from the original configuration file *and* the options from the
|
||||
include.
|
||||
|
||||
Note that this `<<` include merging syntax is only for merging in mappings
|
||||
(configuration options and their values). But if you'd like to include a
|
||||
single value directly, please see the above about standard includes.
|
||||
(configuration options and their values). If you'd like to include a single
|
||||
value directly, please see above about standard includes.
|
||||
|
||||
Additionally, there is a limitation preventing multiple `<<` include merges
|
||||
per file or option value. So for instance, that means you can do one `<<`
|
||||
merge at the global level, another `<<` within each nested option value, etc.
|
||||
(This is a YAML limitation.)
|
||||
|
||||
### Multiple merge includes
|
||||
|
||||
borgmatic has a limitation preventing multiple `<<` include merges per file or
|
||||
option value. This means you can do a single `<<` merge at the global level,
|
||||
another `<<` within each nested option value, etc. (This is a YAML
|
||||
limitation.) For instance:
|
||||
|
||||
```yaml
|
||||
repositories:
|
||||
- path: repo.borg
|
||||
|
||||
# This won't work! You can't do multiple merges like this at the same level.
|
||||
<<: !include common1.yaml
|
||||
<<: !include common2.yaml
|
||||
```
|
||||
|
||||
But read on for a way around this.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.1</span> You can
|
||||
include and merge multiple configuration files all at once. For instance:
|
||||
|
||||
```yaml
|
||||
repositories:
|
||||
- path: repo.borg
|
||||
|
||||
<<: !include [common1.yaml, common2.yaml, common3.yaml]
|
||||
```
|
||||
|
||||
This merges in each included configuration file in turn, such that later files
|
||||
replace the options in earlier ones.
|
||||
|
||||
Here's another way to do the same thing:
|
||||
|
||||
```yaml
|
||||
repositories:
|
||||
- path: repo.borg
|
||||
|
||||
<<: !include
|
||||
- common1.yaml
|
||||
- common2.yaml
|
||||
- common3.yaml
|
||||
```
|
||||
|
||||
|
||||
### Deep merge
|
||||
@@ -309,8 +348,8 @@ source_directories:
|
||||
- /etc
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `location:` section of your configuration.
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put the
|
||||
`source_directories` option in the `location:` section of your configuration.
|
||||
|
||||
Once this include gets merged in, the resulting configuration will have a
|
||||
`source_directories` value of `/etc` and `/var`—with `/home` omitted.
|
||||
@@ -442,8 +481,8 @@ command-line via the `--override` flag. Here's an example:
|
||||
borgmatic create --override remote_path=/usr/local/bin/borg1
|
||||
```
|
||||
|
||||
What this does is load your configuration files, and for each one, disregard
|
||||
the configured value for the `remote_path` option, and use the value of
|
||||
What this does is load your configuration files and for each one, disregard
|
||||
the configured value for the `remote_path` option and use the value of
|
||||
`/usr/local/bin/borg1` instead.
|
||||
|
||||
You can even override nested values or multiple values at once. For instance:
|
||||
@@ -525,7 +564,7 @@ forget to specify the section (like `location:` or `storage:`) that any option
|
||||
is in.
|
||||
|
||||
In this example, when borgmatic runs, all instances of `{user}` get replaced
|
||||
with `foo` and all instances of `{archive-prefix}` get replaced with `bar-`.
|
||||
with `foo` and all instances of `{archive_prefix}` get replaced with `bar-`.
|
||||
(And in this particular example, `{now}` doesn't get replaced with anything,
|
||||
but gets passed directly to Borg.) After substitution, the logical result
|
||||
looks something like this:
|
||||
|
||||
@@ -36,25 +36,24 @@ below for how to configure this.
|
||||
|
||||
### Third-party monitoring services
|
||||
|
||||
borgmatic integrates with monitoring services like
|
||||
[Healthchecks](https://healthchecks.io/), [Cronitor](https://cronitor.io),
|
||||
[Cronhub](https://cronhub.io), [PagerDuty](https://www.pagerduty.com/), and
|
||||
[ntfy](https://ntfy.sh/) and pings these services whenever borgmatic runs.
|
||||
That way, you'll receive an alert when something goes wrong or (for certain
|
||||
hooks) the service doesn't hear from borgmatic for a configured interval. See
|
||||
[Healthchecks
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook),
|
||||
[Cronitor
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook),
|
||||
[Cronhub
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook),
|
||||
[PagerDuty
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook),
|
||||
and [ntfy hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
|
||||
below for how to configure this.
|
||||
borgmatic integrates with these monitoring services and libraries, pinging
|
||||
them as backups happen:
|
||||
|
||||
* [Healthchecks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook)
|
||||
* [Cronitor](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook)
|
||||
* [Cronhub](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook)
|
||||
* [PagerDuty](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook)
|
||||
* [ntfy](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook)
|
||||
* [Grafana Loki](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook)
|
||||
* [Apprise](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook)
|
||||
|
||||
The idea is that you'll receive an alert when something goes wrong or when the
|
||||
service doesn't hear from borgmatic for a configured interval (if supported).
|
||||
See the documentation links above for configuration information.
|
||||
|
||||
While these services and libraries offer different features, you probably only
|
||||
need to use one of them at most.
|
||||
|
||||
While these services offer different features, you probably only need to use
|
||||
one of them at most.
|
||||
|
||||
### Third-party monitoring software
|
||||
|
||||
@@ -106,7 +105,7 @@ on_error:
|
||||
```
|
||||
|
||||
In this example, when the error occurs, borgmatic interpolates runtime values
|
||||
into the hook command: the borgmatic configuration filename, and the path of
|
||||
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
|
||||
@@ -118,7 +117,7 @@ the repository. Here's the full set of supported variables you can use here:
|
||||
occurred without running a command)
|
||||
|
||||
Note that borgmatic runs the `on_error` hooks only for `create`, `prune`,
|
||||
`compact`, or `check` actions or hooks in which an error occurs, and not other
|
||||
`compact`, or `check` actions/hooks in which an error occurs and not other
|
||||
actions. borgmatic does not run `on_error` hooks if an error occurs within a
|
||||
`before_everything` or `after_everything` hook. For more about hooks, see the
|
||||
[borgmatic hooks
|
||||
@@ -129,7 +128,7 @@ 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
|
||||
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:
|
||||
@@ -143,22 +142,20 @@ healthchecks:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
With this hook in place, borgmatic pings your Healthchecks project when a
|
||||
backup begins, ends, or errors. Specifically, after the <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of
|
||||
the `create`, `prune`, `compact`, or `check` actions are run.
|
||||
With this configuration, borgmatic pings your Healthchecks project when a
|
||||
backup begins, ends, or errors, but only when any of the `create`, `prune`,
|
||||
`compact`, or `check` actions are run.
|
||||
|
||||
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
||||
the success after the `after_backup` hooks run, and includes borgmatic logs in
|
||||
the payload data sent to Healthchecks. This means that borgmatic logs show up
|
||||
in the Healthchecks UI, although be aware that Healthchecks currently has a
|
||||
10-kilobyte limit for the logs in each ping.
|
||||
the success and includes borgmatic logs in the payload data sent to
|
||||
Healthchecks. This means that borgmatic logs show up in the Healthchecks UI,
|
||||
although be aware that Healthchecks currently has a 10-kilobyte limit for the
|
||||
logs in each ping.
|
||||
|
||||
If an error occurs during any action or hook, borgmatic notifies Healthchecks
|
||||
after the `on_error` hooks run, also tacking on logs including the error
|
||||
itself. But the logs are only included for errors that occur when a `create`,
|
||||
`prune`, `compact`, or `check` action is run.
|
||||
If an error occurs during any action or hook, borgmatic notifies Healthchecks,
|
||||
also tacking on logs including the error itself. But the logs are only
|
||||
included for errors that occur when a `create`, `prune`, `compact`, or `check`
|
||||
action is run.
|
||||
|
||||
You can customize the verbosity of the logs that are sent to Healthchecks with
|
||||
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags
|
||||
@@ -175,7 +172,7 @@ or it doesn't hear from borgmatic for a certain period of time.
|
||||
## Cronitor hook
|
||||
|
||||
[Cronitor](https://cronitor.io/) provides "Cron monitoring and uptime healthchecks
|
||||
for websites, services and APIs", and borgmatic has built-in
|
||||
for websites, services and APIs," and borgmatic has built-in
|
||||
integration with it. Once you create a Cronitor account and cron job monitor on
|
||||
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||
API URL" for your monitor. Here's an example:
|
||||
@@ -189,14 +186,10 @@ cronitor:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
With this hook in place, borgmatic pings your Cronitor monitor when a backup
|
||||
begins, ends, or errors. Specifically, after the <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||
hooks</a> run, borgmatic lets Cronitor know that it has started if any of the
|
||||
`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
|
||||
complete successfully, borgmatic notifies Cronitor of the success after the
|
||||
`after_backup` hooks run. And if an error occurs during any action or hook,
|
||||
borgmatic notifies Cronitor after the `on_error` hooks run.
|
||||
With this configuration, borgmatic pings your Cronitor monitor when a backup
|
||||
begins, ends, or errors, but only when any of the `prune`, `compact`,
|
||||
`create`, or `check` actions are run. Then, if the actions complete
|
||||
successfully or errors, borgmatic notifies Cronitor accordingly.
|
||||
|
||||
You can configure Cronitor to notify you by a [variety of
|
||||
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups fail
|
||||
@@ -206,7 +199,7 @@ or it doesn't hear from borgmatic for a certain period of time.
|
||||
## Cronhub hook
|
||||
|
||||
[Cronhub](https://cronhub.io/) provides "instant alerts when any of your
|
||||
background jobs fail silently or run longer than expected", and borgmatic has
|
||||
background jobs fail silently or run longer than expected," and borgmatic has
|
||||
built-in integration with it. Once you create a Cronhub account and monitor on
|
||||
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||
URL" for your monitor. Here's an example:
|
||||
@@ -220,14 +213,10 @@ cronhub:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
With this hook in place, borgmatic pings your Cronhub monitor when a backup
|
||||
begins, ends, or errors. Specifically, after the <a
|
||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
||||
hooks</a> run, borgmatic lets Cronhub know that it has started if any of the
|
||||
`prune`, `compact`, `create`, or `check` actions are run. Then, if the actions
|
||||
complete successfully, borgmatic notifies Cronhub of the success after the
|
||||
`after_backup` hooks run. And if an error occurs during any action or hook,
|
||||
borgmatic notifies Cronhub after the `on_error` hooks run.
|
||||
With this configuration, borgmatic pings your Cronhub monitor when a backup
|
||||
begins, ends, or errors, but only when any of the `prune`, `compact`,
|
||||
`create`, or `check` actions are run. Then, if the actions complete
|
||||
successfully or errors, borgmatic notifies Cronhub accordingly.
|
||||
|
||||
Note that even though you configure borgmatic with the "start" variant of the
|
||||
ping URL, borgmatic substitutes the correct state into the URL when pinging
|
||||
@@ -265,11 +254,10 @@ pagerduty:
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
With this hook in place, borgmatic creates a PagerDuty event for your service
|
||||
whenever backups fail. Specifically, if an error occurs during a `create`,
|
||||
`prune`, `compact`, or `check` action, borgmatic sends an event to PagerDuty
|
||||
before the `on_error` hooks run. Note that borgmatic does not contact
|
||||
PagerDuty when a backup starts or ends without error.
|
||||
With this configuration, borgmatic creates a PagerDuty event for your service
|
||||
whenever backups fail, but only when any of the `create`, `prune`, `compact`,
|
||||
or `check` actions are run. Note that borgmatic does not contact PagerDuty
|
||||
when a backup starts or when it ends without error.
|
||||
|
||||
You can configure PagerDuty to notify you by a [variety of
|
||||
mechanisms](https://support.pagerduty.com/docs/notifications) when backups
|
||||
@@ -281,10 +269,12 @@ us](https://torsion.org/borgmatic/#support-and-contributing).
|
||||
|
||||
## ntfy hook
|
||||
|
||||
[ntfy](https://ntfy.sh) is a free, simple, service (either hosted or self-hosted)
|
||||
which offers simple pub/sub push notifications to multiple platforms including
|
||||
[web](https://ntfy.sh/stats), [Android](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
|
||||
and [iOS](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||
<span class="minilink minilink-addedin">New in version 1.6.3</span>
|
||||
[ntfy](https://ntfy.sh) is a free, simple, service (either hosted or
|
||||
self-hosted) which offers simple pub/sub push notifications to multiple
|
||||
platforms including [web](https://ntfy.sh/stats),
|
||||
[Android](https://play.google.com/store/apps/details?id=io.heckel.ntfy) and
|
||||
[iOS](https://apps.apple.com/us/app/ntfy/id1625396347).
|
||||
|
||||
Since push notifications for regular events might soon become quite annoying,
|
||||
this hook only fires on any errors by default in order to instantly alert you to issues.
|
||||
@@ -304,17 +294,17 @@ ntfy:
|
||||
topic: my-unique-topic
|
||||
server: https://ntfy.my-domain.com
|
||||
start:
|
||||
title: A Borgmatic backup started
|
||||
title: A borgmatic backup started
|
||||
message: Watch this space...
|
||||
tags: borgmatic
|
||||
priority: min
|
||||
finish:
|
||||
title: A Borgmatic backup completed successfully
|
||||
title: A borgmatic backup completed successfully
|
||||
message: Nice!
|
||||
tags: borgmatic,+1
|
||||
priority: min
|
||||
fail:
|
||||
title: A Borgmatic backup failed
|
||||
title: A borgmatic backup failed
|
||||
message: You should probably fix it
|
||||
tags: borgmatic,-1,skull
|
||||
priority: max
|
||||
@@ -328,6 +318,122 @@ ntfy:
|
||||
the `ntfy:` option in the `hooks:` section of your configuration.
|
||||
|
||||
|
||||
## Loki hook
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.3</span> [Grafana
|
||||
Loki](https://grafana.com/oss/loki/) is a "horizontally scalable, highly
|
||||
available, multi-tenant log aggregation system inspired by Prometheus."
|
||||
borgmatic has built-in integration with Loki, sending both backup status and
|
||||
borgmatic logs.
|
||||
|
||||
You can configure borgmatic to use either a [self-hosted Loki
|
||||
instance](https://grafana.com/docs/loki/latest/installation/) or [a Grafana
|
||||
Cloud account](https://grafana.com/auth/sign-up/create-user). Start by setting
|
||||
your Loki API push URL. Here's an example:
|
||||
|
||||
```yaml
|
||||
loki:
|
||||
url: http://localhost:3100/loki/api/v1/push
|
||||
```
|
||||
|
||||
With this configuration, borgmatic sends its logs to your Loki instance as any
|
||||
of the `prune`, `compact`, `create`, or `check` actions are run. Then, after
|
||||
the actions complete, borgmatic notifies Loki of success or failure.
|
||||
|
||||
This hook supports sending arbitrary labels to Loki. For instance:
|
||||
|
||||
```yaml
|
||||
loki:
|
||||
url: http://localhost:3100/loki/api/v1/push
|
||||
|
||||
labels:
|
||||
app: borgmatic
|
||||
hostname: example.org
|
||||
```
|
||||
|
||||
There are also a few placeholders you can optionally use as label values:
|
||||
|
||||
* `__config`: name of the borgmatic configuration file
|
||||
* `__config_path`: full path of the borgmatic configuration file
|
||||
* `__hostname`: the local machine hostname
|
||||
|
||||
These placeholders are only substituted for the whole label value, not
|
||||
interpolated into a larger string. For instance:
|
||||
|
||||
```yaml
|
||||
loki:
|
||||
url: http://localhost:3100/loki/api/v1/push
|
||||
|
||||
labels:
|
||||
app: borgmatic
|
||||
config: __config
|
||||
hostname: __hostname
|
||||
```
|
||||
|
||||
|
||||
## Apprise hook
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.4</span>
|
||||
[Apprise](https://github.com/caronc/apprise/wiki) is a local notification library
|
||||
that "allows you to send a notification to almost all of the most popular
|
||||
[notification services](https://github.com/caronc/apprise/wiki) available to
|
||||
us today such as: Telegram, Discord, Slack, Amazon SNS, Gotify, etc."
|
||||
|
||||
Depending on how you installed borgmatic, it may not have come with Apprise.
|
||||
For instance, if you originally [installed borgmatic with
|
||||
pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation),
|
||||
run the following to install Apprise so borgmatic can use it:
|
||||
|
||||
```bash
|
||||
sudo pipx install --editable --force borgmatic[Apprise]
|
||||
```
|
||||
|
||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||
|
||||
Once Apprise is installed, configure borgmatic to notify one or more [Apprise
|
||||
services](https://github.com/caronc/apprise/wiki). For example:
|
||||
|
||||
```yaml
|
||||
apprise:
|
||||
services:
|
||||
- url: gotify://hostname/token
|
||||
label: gotify
|
||||
- url: mastodons://access_key@hostname/@user
|
||||
label: mastodon
|
||||
```
|
||||
|
||||
With this configuration, borgmatic pings each of the configured Apprise
|
||||
services when a backup begins, ends, or errors, but only when any of the
|
||||
`prune`, `compact`, `create`, or `check` actions are run.
|
||||
|
||||
You can optionally customize the contents of the default messages sent to
|
||||
these services:
|
||||
|
||||
```yaml
|
||||
apprise:
|
||||
services:
|
||||
- url: gotify://hostname/token
|
||||
label: gotify
|
||||
start:
|
||||
title: Ping!
|
||||
body: Starting backup process.
|
||||
finish:
|
||||
title: Ping!
|
||||
body: Backups successfully made.
|
||||
fail:
|
||||
title: Ping!
|
||||
body: Your backups have failed.
|
||||
states:
|
||||
- start
|
||||
- finish
|
||||
- fail
|
||||
```
|
||||
|
||||
See the [configuration
|
||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
|
||||
details.
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
@@ -336,7 +442,7 @@ 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.
|
||||
output only shows up at the console and not in syslog.
|
||||
|
||||
|
||||
### Latest backups
|
||||
|
||||
@@ -9,9 +9,15 @@ eleventyNavigation:
|
||||
|
||||
If you want to use a Borg repository passphrase or database passwords with
|
||||
borgmatic, you can set them directly in your borgmatic configuration file,
|
||||
treating those secrets like any other option value. But if you'd rather store
|
||||
them outside of borgmatic, whether for convenience or security reasons, read
|
||||
on.
|
||||
treating those secrets like any other option value. For instance, you can
|
||||
specify your Borg passhprase with:
|
||||
|
||||
```yaml
|
||||
encryption_passphrase: yourpassphrase
|
||||
```
|
||||
|
||||
But if you'd rather store them outside of borgmatic, whether for convenience
|
||||
or security reasons, read on.
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.6.4</span> borgmatic
|
||||
supports interpolating arbitrary environment variables directly into option
|
||||
@@ -20,14 +26,14 @@ pull your repository passphrase, your database passwords, or any other option
|
||||
values from environment variables. For instance:
|
||||
|
||||
```yaml
|
||||
encryption_passphrase: ${MY_PASSPHRASE}
|
||||
encryption_passphrase: ${YOUR_PASSPHRASE}
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `storage:` section of your configuration.
|
||||
|
||||
This uses the `MY_PASSPHRASE` environment variable as your encryption
|
||||
passphrase. Note that the `{` `}` brackets are required. `$MY_PASSPHRASE` by
|
||||
This uses the `YOUR_PASSPHRASE` environment variable as your encryption
|
||||
passphrase. Note that the `{` `}` brackets are required. `$YOUR_PASSPHRASE` by
|
||||
itself will not work.
|
||||
|
||||
In the case of `encryption_passphrase` in particular, an alternate approach
|
||||
@@ -42,25 +48,26 @@ the same approach applies. For example:
|
||||
```yaml
|
||||
postgresql_databases:
|
||||
- name: users
|
||||
password: ${MY_DATABASE_PASSWORD}
|
||||
password: ${YOUR_DATABASE_PASSWORD}
|
||||
```
|
||||
|
||||
<span class="minilink minilink-addedin">Prior to version 1.8.0</span> Put
|
||||
this option in the `hooks:` section of your configuration.
|
||||
|
||||
This uses the `MY_DATABASE_PASSWORD` environment variable as your database
|
||||
This uses the `YOUR_DATABASE_PASSWORD` environment variable as your database
|
||||
password.
|
||||
|
||||
|
||||
### Interpolation defaults
|
||||
|
||||
If you'd like to set a default for your environment variables, you can do so with the following syntax:
|
||||
If you'd like to set a default for your environment variables, you can do so
|
||||
with the following syntax:
|
||||
|
||||
```yaml
|
||||
encryption_passphrase: ${MY_PASSPHRASE:-defaultpass}
|
||||
encryption_passphrase: ${YOUR_PASSPHRASE:-defaultpass}
|
||||
```
|
||||
|
||||
Here, "`defaultpass`" is the default passphrase if the `MY_PASSPHRASE`
|
||||
Here, "`defaultpass`" is the default passphrase if the `YOUR_PASSPHRASE`
|
||||
environment variable is not set. Without a default, if the environment
|
||||
variable doesn't exist, borgmatic will error.
|
||||
|
||||
@@ -90,3 +97,9 @@ Additionally, borgmatic action hooks support their own [variable
|
||||
interpolation](https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/#variable-interpolation),
|
||||
although in that case it's for particular borgmatic runtime values rather than
|
||||
(only) environment variables.
|
||||
|
||||
Lastly, if you do want to specify your passhprase directly within borgmatic
|
||||
configuration, but you'd like to keep it in a separate file from your main
|
||||
configuration, you can [use a configuration include or a merge
|
||||
include](https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#configuration-includes)
|
||||
to pull in an external password.
|
||||
|
||||
@@ -7,74 +7,70 @@ eleventyNavigation:
|
||||
---
|
||||
## Installation
|
||||
|
||||
Many users need to backup system files that require privileged access, so
|
||||
these instructions install and run borgmatic as root. If you don't need to
|
||||
backup such files, then you are welcome to install and run borgmatic as a
|
||||
non-root user.
|
||||
### Prerequisites
|
||||
|
||||
First, manually [install
|
||||
First, [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||
version 1.1. borgmatic does not install Borg automatically so as to avoid
|
||||
conflicts with existing Borg installations.
|
||||
|
||||
Then, download and install borgmatic as a [user site
|
||||
installation](https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site)
|
||||
by running the following command:
|
||||
Then, [install pipx](https://pypa.github.io/pipx/installation/) as the root
|
||||
user (with `sudo`) to make installing borgmatic easy without impacting other
|
||||
Python applications on your system. If you have trouble installing pipx with
|
||||
pip, then you can install a system package instead. E.g. on Ubuntu or Debian,
|
||||
run:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --user --upgrade borgmatic
|
||||
sudo apt update
|
||||
sudo apt install pipx
|
||||
```
|
||||
|
||||
This installs borgmatic and its commands at the `/root/.local/bin` path.
|
||||
### Root install
|
||||
|
||||
Your pip binary may have a different name than "pip3". Make sure you're using
|
||||
Python 3.7+, as borgmatic does not support older versions of Python.
|
||||
|
||||
The next step is to ensure that borgmatic's commands available are on your
|
||||
system `PATH`, so that you can run borgmatic:
|
||||
If you want to run borgmatic on a schedule with privileged access to your
|
||||
files, then you should install borgmatic as the root user by running the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
echo export 'PATH="$PATH:/root/.local/bin"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
sudo pipx ensurepath
|
||||
sudo pipx install borgmatic
|
||||
```
|
||||
|
||||
This adds `/root/.local/bin` to your non-root user's system `PATH`.
|
||||
|
||||
If you're using a command shell other than Bash, you may need to use different
|
||||
commands here.
|
||||
|
||||
You can check whether all of this worked with:
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
sudo borgmatic --version
|
||||
sudo su -
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
|
||||
As an alternative to adding the path to `~/.bashrc` file, if you're using sudo
|
||||
to run borgmatic, you can configure [sudo's
|
||||
`secure_path` option](https://man.archlinux.org/man/sudoers.5) to include
|
||||
borgmatic's path.
|
||||
And if you'd also like `sudo borgmatic` to work, keep reading!
|
||||
|
||||
|
||||
### Global install option
|
||||
### Non-root install
|
||||
|
||||
If you try the user site installation above, and have problems making
|
||||
borgmatic commands runnable on your system `PATH`, an alternate approach is to
|
||||
install borgmatic globally.
|
||||
|
||||
The following uninstalls borgmatic, and then reinstalls it such that borgmatic
|
||||
commands are on the default system `PATH`:
|
||||
If you only want to run borgmatic as a non-root user (without privileged file
|
||||
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||
then install borgmatic as a non-root user by running the following commands as
|
||||
that user:
|
||||
|
||||
```bash
|
||||
sudo pip3 uninstall borgmatic
|
||||
sudo pip3 install --upgrade borgmatic
|
||||
pipx ensurepath
|
||||
pipx install borgmatic
|
||||
```
|
||||
|
||||
The main downside of a global install is that borgmatic is less cleanly
|
||||
separated from the rest of your Python software, and there's the theoretical
|
||||
possibility of library conflicts. But if you're okay with that, for instance
|
||||
on a relatively dedicated system, then a global install can work out fine.
|
||||
This should work even if you've also installed borgmatic as the root user.
|
||||
|
||||
Check whether this worked with:
|
||||
|
||||
```bash
|
||||
borgmatic --version
|
||||
```
|
||||
|
||||
If borgmatic is properly installed, that should output your borgmatic version.
|
||||
You can also try `sudo borgmatic --version` if you intend to run borgmatic
|
||||
with `sudo`. If that doesn't work, you may need to update your [sudoers
|
||||
`secure_path` option](https://wiki.archlinux.org/title/Sudo).
|
||||
|
||||
|
||||
### Other ways to install
|
||||
@@ -229,7 +225,7 @@ sudo borgmatic rcreate --encryption repokey-aes-ocb
|
||||
certain platforms like ARM64.)
|
||||
|
||||
This uses the borgmatic configuration file you created above to determine
|
||||
which local or remote repository to create, and encrypts it with the
|
||||
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/stable/usage/init.html#encryption-mode-tldr)
|
||||
@@ -286,6 +282,21 @@ due to things like file damage. For instance:
|
||||
sudo borgmatic --verbosity 1 --list --stats
|
||||
```
|
||||
|
||||
### Skipping actions
|
||||
|
||||
<span class="minilink minilink-addedin">New in version 1.8.5</span> You can
|
||||
configure borgmatic to skip running certain actions (default or otherwise).
|
||||
For instance, to always skip the `compact` action when using [Borg's
|
||||
append-only
|
||||
mode](https://borgbackup.readthedocs.io/en/stable/usage/notes.html#append-only-mode-forbid-compaction),
|
||||
set the `skip_actions` option:
|
||||
|
||||
```
|
||||
skip_actions:
|
||||
- compact
|
||||
```
|
||||
|
||||
|
||||
## Autopilot
|
||||
|
||||
Running backups manually is good for validating your configuration, but I'm
|
||||
|
||||
+29
-52
@@ -7,26 +7,38 @@ eleventyNavigation:
|
||||
---
|
||||
## Upgrading borgmatic
|
||||
|
||||
In general, all you should need to do to upgrade borgmatic is run the
|
||||
following:
|
||||
In general, all you should need to do to upgrade borgmatic if you've
|
||||
[installed it with
|
||||
pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation)
|
||||
is to run the following:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --user --upgrade borgmatic
|
||||
sudo pipx upgrade borgmatic
|
||||
```
|
||||
|
||||
See below about special cases with old versions of borgmatic. Additionally, if
|
||||
you installed borgmatic [without using `pip3 install
|
||||
--user`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
|
||||
then your upgrade process may be different.
|
||||
Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||
upgrade each installation independently.
|
||||
|
||||
If you originally installed borgmatic with `sudo pip3 install --user`, you can
|
||||
uninstall it first with `sudo pip3 uninstall borgmatic` and then [install it
|
||||
again with
|
||||
pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation),
|
||||
which should better isolate borgmatic from your other Python applications.
|
||||
|
||||
But if you [installed borgmatic without pipx or
|
||||
pip3](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
|
||||
then your upgrade method may be different.
|
||||
|
||||
|
||||
|
||||
### Upgrading your configuration
|
||||
|
||||
The borgmatic configuration file format is almost always backwards-compatible
|
||||
from release to release without any changes, but you may still want to update
|
||||
your configuration file when you upgrade to take advantage of new
|
||||
configuration options. This is completely optional. If you prefer, you can add
|
||||
new configuration options manually.
|
||||
The borgmatic configuration file format is usually backwards-compatible from
|
||||
release to release without any changes, but you may still want to update your
|
||||
configuration file when you upgrade to take advantage of new configuration
|
||||
options or avoid old configuration from eventually becoming unsupported. If
|
||||
you prefer, you can add new configuration options manually.
|
||||
|
||||
If you do want to upgrade your configuration file to include new options, use
|
||||
the `borgmatic config generate` action with its optional `--source` flag that
|
||||
@@ -63,46 +75,11 @@ and, if desired, replace your original configuration file with it.
|
||||
### Upgrading from borgmatic 1.0.x
|
||||
|
||||
borgmatic changed its configuration file format in version 1.1.0 from
|
||||
INI-style to YAML. This better supports validation, and has a more natural way
|
||||
to express lists of values. To upgrade your existing configuration, first
|
||||
upgrade to the last version of borgmatic to support converting configuration:
|
||||
borgmatic 1.7.14.
|
||||
|
||||
As of version 1.1.0, borgmatic no longer supports Python 2. If you were
|
||||
already running borgmatic with Python 3, then you can upgrade borgmatic
|
||||
in-place:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --user --upgrade borgmatic==1.7.14
|
||||
```
|
||||
|
||||
But if you were running borgmatic with Python 2, uninstall and reinstall instead:
|
||||
|
||||
```bash
|
||||
sudo pip uninstall borgmatic
|
||||
sudo pip3 install --user borgmatic==1.7.14
|
||||
```
|
||||
|
||||
The pip binary names for different versions of Python can differ, so the above
|
||||
commands may need some tweaking to work on your machine.
|
||||
|
||||
|
||||
Once borgmatic is upgraded, run:
|
||||
|
||||
```bash
|
||||
sudo upgrade-borgmatic-config
|
||||
```
|
||||
|
||||
That will generate a new YAML configuration file at /etc/borgmatic/config.yaml
|
||||
(by default) using the values from both your existing configuration and
|
||||
excludes files. The new version of borgmatic will consume the YAML
|
||||
configuration file instead of the old one.
|
||||
|
||||
Now you can upgrade to a newer version of borgmatic:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --user borgmatic
|
||||
```
|
||||
INI-style to YAML. This better supports validation and has a more natural way
|
||||
to express lists of values. Modern versions of borgmatic no longer include
|
||||
support for upgrading configuration files this old, but feel free to [file a
|
||||
ticket](https://torsion.org/borgmatic/#issues) for help with upgrading any old
|
||||
INI-style configuration files you may have.
|
||||
|
||||
|
||||
## Upgrading Borg
|
||||
|
||||
@@ -21,5 +21,3 @@ version](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#configuration
|
||||
```yaml
|
||||
{% include borgmatic/config.yaml %}
|
||||
```
|
||||
|
||||
Note that you can also [download this configuration
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Source code reference
|
||||
eleventyNavigation:
|
||||
key: 🐍 Source code reference
|
||||
parent: Reference guides
|
||||
order: 3
|
||||
---
|
||||
## getting oriented
|
||||
|
||||
If case you're interested in [developing on
|
||||
borgmatic](https://torsion.org/borgmatic/docs/how-to/develop-on-borgmatic/),
|
||||
here's an abridged primer on how its Python source code is organized to help
|
||||
you get started. At the top level we have:
|
||||
|
||||
* [borgmatic](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic): The main borgmatic source module. Most of the code is here.
|
||||
* [docs](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/docs): How-to and reference documentation, including the document you're reading now.
|
||||
* [sample](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/sample): Example configurations for cron and systemd.
|
||||
* [scripts](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/scripts): Dev-facing scripts for things like building documentation and running end-to-end tests.
|
||||
* [tests](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/tests): Automated tests organized by: end-to-end, integration, and unit.
|
||||
|
||||
Within the `borgmatic` directory you'll find:
|
||||
|
||||
* [actions](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/actions): Mid-level code for running each borgmatic action (create, list, check, etc.).
|
||||
* [borg](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/borg): Lower-level code that actually shells out to Borg for each action.
|
||||
* [commands](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/commands): Looking to add a new flag or action? Start here. This contains borgmatic's entry point, argument parsing, and shell completion.
|
||||
* [config](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/config): Code responsible for loading, normalizing, and validating borgmatic's configuration.
|
||||
* [hooks](https://projects.torsion.org/borgmatic-collective/borgmatic/src/branch/main/borgmatic/hooks): Looking to add a new database or monitoring integration? Start here.
|
||||
|
||||
So, broadly speaking, the control flow goes: `commands` → `config` followed by `commands` → `actions` → `borg` and `hooks`.
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -32,16 +32,16 @@ RestrictSUIDSGID=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallErrorNumber=EPERM
|
||||
# To restrict write access further, change "ProtectSystem" to "strict" and uncomment
|
||||
# "ReadWritePaths", "ReadOnlyPaths", "ProtectHome", and "BindPaths". Then add any local repository
|
||||
# paths to the list of "ReadWritePaths" and local backup source paths to "ReadOnlyPaths". This
|
||||
# leaves most of the filesystem read-only to borgmatic.
|
||||
# To restrict write access further, change "ProtectSystem" to "strict" and
|
||||
# uncomment "ReadWritePaths", "TemporaryFileSystem", "BindPaths" and
|
||||
# "BindReadOnlyPaths". Then add any local repository paths to the list of
|
||||
# "ReadWritePaths". This leaves most of the filesystem read-only to borgmatic.
|
||||
ProtectSystem=full
|
||||
# ReadWritePaths=-/mnt/my_backup_drive
|
||||
# ReadOnlyPaths=-/var/lib/my_backup_source
|
||||
# This will mount a tmpfs on top of /root and pass through needed paths
|
||||
# ProtectHome=tmpfs
|
||||
# TemporaryFileSystem=/root:ro
|
||||
# BindPaths=-/root/.cache/borg -/root/.config/borg -/root/.borgmatic
|
||||
# BindReadOnlyPaths=-/root/.ssh
|
||||
|
||||
# May interfere with running external programs within borgmatic hooks.
|
||||
CapabilityBoundingSet=CAP_DAC_READ_SEARCH CAP_NET_RAW
|
||||
|
||||
@@ -18,11 +18,11 @@ if [ -z "$TEST_CONTAINER" ]; then
|
||||
fi
|
||||
|
||||
apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client mongodb-tools \
|
||||
py3-ruamel.yaml py3-ruamel.yaml.clib bash sqlite fish
|
||||
py3-ruamel.yaml py3-ruamel.yaml.clib py3-yaml bash sqlite fish
|
||||
# If certain dependencies of black are available in this version of Alpine, install them.
|
||||
apk add --no-cache py3-typed-ast py3-regex || true
|
||||
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1
|
||||
pip3 install --ignore-installed tox==3.25.1
|
||||
pip3 install --ignore-installed tox==4.11.3
|
||||
export COVERAGE_FILE=/tmp/.coverage
|
||||
|
||||
if [ "$1" != "--end-to-end-only" ]; then
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.8.0'
|
||||
VERSION = '1.8.5'
|
||||
|
||||
|
||||
setup(
|
||||
@@ -33,9 +33,10 @@ setup(
|
||||
'jsonschema',
|
||||
'packaging',
|
||||
'requests',
|
||||
'ruamel.yaml>0.15.0,<0.18.0',
|
||||
'ruamel.yaml>0.15.0',
|
||||
'setuptools',
|
||||
),
|
||||
extras_require={"Apprise": ["apprise"]},
|
||||
include_package_data=True,
|
||||
python_requires='>=3.7',
|
||||
python_requires='>=3.8',
|
||||
)
|
||||
|
||||
+10
-9
@@ -1,6 +1,8 @@
|
||||
appdirs==1.4.4; python_version >= '3.8'
|
||||
apprise==1.3.0
|
||||
attrs==22.2.0; python_version >= '3.8'
|
||||
black==23.3.0; python_version >= '3.8'
|
||||
certifi==2023.7.22
|
||||
chardet==5.1.0
|
||||
click==8.1.3; python_version >= '3.8'
|
||||
codespell==2.2.4
|
||||
@@ -12,22 +14,21 @@ flake8-use-fstring==1.4
|
||||
flake8-variables-names==0.0.5
|
||||
flexmock==0.11.3
|
||||
idna==3.4
|
||||
importlib_metadata==6.3.0; python_version < '3.8'
|
||||
isort==5.12.0
|
||||
jsonschema==4.17.3
|
||||
Markdown==3.4.1
|
||||
mccabe==0.7.0
|
||||
packaging==23.1
|
||||
pathspec==0.11.1
|
||||
pluggy==1.0.0
|
||||
pathspec==0.11.1; python_version >= '3.8'
|
||||
py==1.11.0
|
||||
pycodestyle==2.10.0
|
||||
pyflakes==3.0.1
|
||||
jsonschema==4.17.3
|
||||
pytest==7.3.0
|
||||
pytest-cov==4.0.0
|
||||
regex; python_version >= '3.8'
|
||||
PyYAML>5.0.0
|
||||
regex
|
||||
requests==2.31.0
|
||||
ruamel.yaml>0.15.0,<0.18.0
|
||||
toml==0.10.2; python_version >= '3.8'
|
||||
typed-ast; python_version >= '3.8'
|
||||
typing-extensions==4.5.0; python_version < '3.8'
|
||||
zipp==3.15.0; python_version < '3.8'
|
||||
ruamel.yaml>0.15.0
|
||||
toml==0.10.2
|
||||
typed-ast
|
||||
|
||||
@@ -10,18 +10,28 @@ services:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: test2
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_USER: postgres2
|
||||
command: docker-entrypoint.sh -p 5433
|
||||
mysql:
|
||||
image: docker.io/mariadb:10.5
|
||||
mariadb:
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: test
|
||||
MYSQL_DATABASE: test
|
||||
mysql2:
|
||||
image: docker.io/mariadb:10.5
|
||||
MARIADB_ROOT_PASSWORD: test
|
||||
MARIADB_DATABASE: test
|
||||
mariadb2:
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: test2
|
||||
MYSQL_DATABASE: test
|
||||
MARIADB_ROOT_PASSWORD: test2
|
||||
MARIADB_DATABASE: test
|
||||
command: docker-entrypoint.sh --port=3307
|
||||
not-actually-mysql:
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: test
|
||||
MARIADB_DATABASE: test
|
||||
not-actually-mysql2:
|
||||
image: docker.io/mariadb:10.11.4
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: test2
|
||||
MARIADB_DATABASE: test
|
||||
command: docker-entrypoint.sh --port=3307
|
||||
mongodb:
|
||||
image: docker.io/mongo:5.0.5
|
||||
@@ -50,7 +60,7 @@ services:
|
||||
depends_on:
|
||||
- postgresql
|
||||
- postgresql2
|
||||
- mysql
|
||||
- mysql2
|
||||
- mariadb
|
||||
- mariadb2
|
||||
- mongodb
|
||||
- mongodb2
|
||||
|
||||
@@ -5,7 +5,9 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pymongo
|
||||
import pytest
|
||||
import ruamel.yaml
|
||||
|
||||
|
||||
def write_configuration(
|
||||
@@ -21,7 +23,7 @@ def write_configuration(
|
||||
for testing. This includes injecting the given repository path, borgmatic source directory for
|
||||
storing database dumps, dump format (for PostgreSQL), and encryption passphrase.
|
||||
'''
|
||||
config = f'''
|
||||
config_yaml = f'''
|
||||
source_directories:
|
||||
- {source_directory}
|
||||
repositories:
|
||||
@@ -45,18 +47,32 @@ postgresql_databases:
|
||||
hostname: postgresql
|
||||
username: postgres
|
||||
password: test
|
||||
mysql_databases:
|
||||
mariadb_databases:
|
||||
- name: test
|
||||
hostname: mysql
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
hostname: mysql
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
format: sql
|
||||
hostname: mysql
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
mysql_databases:
|
||||
- name: test
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
- name: all
|
||||
format: sql
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
mongodb_databases:
|
||||
@@ -76,7 +92,9 @@ sqlite_databases:
|
||||
'''
|
||||
|
||||
with open(config_path, 'w') as config_file:
|
||||
config_file.write(config)
|
||||
config_file.write(config_yaml)
|
||||
|
||||
return ruamel.yaml.YAML(typ='safe').load(config_yaml)
|
||||
|
||||
|
||||
def write_custom_restore_configuration(
|
||||
@@ -92,7 +110,7 @@ def write_custom_restore_configuration(
|
||||
for testing with custom restore options. This includes a custom restore_hostname, restore_port,
|
||||
restore_username, restore_password and restore_path.
|
||||
'''
|
||||
config = f'''
|
||||
config_yaml = f'''
|
||||
source_directories:
|
||||
- {source_directory}
|
||||
repositories:
|
||||
@@ -109,14 +127,22 @@ postgresql_databases:
|
||||
format: {postgresql_dump_format}
|
||||
restore_hostname: postgresql2
|
||||
restore_port: 5433
|
||||
restore_username: postgres2
|
||||
restore_password: test2
|
||||
mariadb_databases:
|
||||
- name: test
|
||||
hostname: mariadb
|
||||
username: root
|
||||
password: test
|
||||
restore_hostname: mariadb2
|
||||
restore_port: 3307
|
||||
restore_username: root
|
||||
restore_password: test2
|
||||
mysql_databases:
|
||||
- name: test
|
||||
hostname: mysql
|
||||
hostname: not-actually-mysql
|
||||
username: root
|
||||
password: test
|
||||
restore_hostname: mysql2
|
||||
restore_hostname: not-actually-mysql2
|
||||
restore_port: 3307
|
||||
restore_username: root
|
||||
restore_password: test2
|
||||
@@ -138,7 +164,9 @@ sqlite_databases:
|
||||
'''
|
||||
|
||||
with open(config_path, 'w') as config_file:
|
||||
config_file.write(config)
|
||||
config_file.write(config_yaml)
|
||||
|
||||
return ruamel.yaml.YAML(typ='safe').load(config_yaml)
|
||||
|
||||
|
||||
def write_simple_custom_restore_configuration(
|
||||
@@ -154,7 +182,7 @@ def write_simple_custom_restore_configuration(
|
||||
custom restore_hostname, restore_port, restore_username and restore_password as we only test
|
||||
these options for PostgreSQL.
|
||||
'''
|
||||
config = f'''
|
||||
config_yaml = f'''
|
||||
source_directories:
|
||||
- {source_directory}
|
||||
repositories:
|
||||
@@ -172,7 +200,147 @@ postgresql_databases:
|
||||
'''
|
||||
|
||||
with open(config_path, 'w') as config_file:
|
||||
config_file.write(config)
|
||||
config_file.write(config_yaml)
|
||||
|
||||
return ruamel.yaml.YAML(typ='safe').load(config_yaml)
|
||||
|
||||
|
||||
def get_connection_params(database, use_restore_options=False):
|
||||
hostname = (database.get('restore_hostname') if use_restore_options else None) or database.get(
|
||||
'hostname'
|
||||
)
|
||||
port = (database.get('restore_port') if use_restore_options else None) or database.get('port')
|
||||
username = (database.get('restore_username') if use_restore_options else None) or database.get(
|
||||
'username'
|
||||
)
|
||||
password = (database.get('restore_password') if use_restore_options else None) or database.get(
|
||||
'password'
|
||||
)
|
||||
|
||||
return (hostname, port, username, password)
|
||||
|
||||
|
||||
def run_postgresql_command(command, config, use_restore_options=False):
|
||||
(hostname, port, username, password) = get_connection_params(
|
||||
config['postgresql_databases'][0], use_restore_options
|
||||
)
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
'/usr/bin/psql',
|
||||
f'--host={hostname}',
|
||||
f'--port={port or 5432}',
|
||||
f"--username={username or 'root'}",
|
||||
f'--command={command}',
|
||||
'test',
|
||||
],
|
||||
env={'PGPASSWORD': password},
|
||||
)
|
||||
|
||||
|
||||
def run_mariadb_command(command, config, use_restore_options=False, binary_name='mariadb'):
|
||||
(hostname, port, username, password) = get_connection_params(
|
||||
config[f'{binary_name}_databases'][0], use_restore_options
|
||||
)
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
f'/usr/bin/{binary_name}',
|
||||
f'--host={hostname}',
|
||||
f'--port={port or 3306}',
|
||||
f'--user={username}',
|
||||
f'--execute={command}',
|
||||
'test',
|
||||
],
|
||||
env={'MYSQL_PWD': password},
|
||||
)
|
||||
|
||||
|
||||
def get_mongodb_database_client(config, use_restore_options=False):
|
||||
(hostname, port, username, password) = get_connection_params(
|
||||
config['mongodb_databases'][0], use_restore_options
|
||||
)
|
||||
|
||||
return pymongo.MongoClient(f'mongodb://{username}:{password}@{hostname}:{port or 27017}').test
|
||||
|
||||
|
||||
def run_sqlite_command(command, config, use_restore_options=False):
|
||||
database = config['sqlite_databases'][0]
|
||||
path = (database.get('restore_path') if use_restore_options else None) or database.get('path')
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
'/usr/bin/sqlite3',
|
||||
path,
|
||||
command,
|
||||
'.exit',
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_HOOK_NAMES = {'postgresql', 'mariadb', 'mysql', 'mongodb', 'sqlite'}
|
||||
|
||||
|
||||
def create_test_tables(config, use_restore_options=False):
|
||||
'''
|
||||
Create test tables for borgmatic to dump and backup.
|
||||
'''
|
||||
command = 'create table test{id} (thing int); insert into test{id} values (1);'
|
||||
|
||||
if 'postgresql_databases' in config:
|
||||
run_postgresql_command(command.format(id=1), config, use_restore_options)
|
||||
if 'mariadb_databases' in config:
|
||||
run_mariadb_command(command.format(id=2), config, use_restore_options)
|
||||
if 'mysql_databases' in config:
|
||||
run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')
|
||||
if 'mongodb_databases' in config:
|
||||
get_mongodb_database_client(config, use_restore_options)['test4'].insert_one({'thing': 1})
|
||||
if 'sqlite_databases' in config:
|
||||
run_sqlite_command(command.format(id=5), config, use_restore_options)
|
||||
|
||||
|
||||
def drop_test_tables(config, use_restore_options=False):
|
||||
'''
|
||||
Drop the test tables in preparation for borgmatic restoring them.
|
||||
'''
|
||||
command = 'drop table if exists test{id};'
|
||||
|
||||
if 'postgresql_databases' in config:
|
||||
run_postgresql_command(command.format(id=1), config, use_restore_options)
|
||||
if 'mariadb_databases' in config:
|
||||
run_mariadb_command(command.format(id=2), config, use_restore_options)
|
||||
if 'mysql_databases' in config:
|
||||
run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')
|
||||
if 'mongodb_databases' in config:
|
||||
get_mongodb_database_client(config, use_restore_options)['test4'].drop()
|
||||
if 'sqlite_databases' in config:
|
||||
run_sqlite_command(command.format(id=5), config, use_restore_options)
|
||||
|
||||
|
||||
def select_test_tables(config, use_restore_options=False):
|
||||
'''
|
||||
Select the test tables to make sure they exist.
|
||||
|
||||
Raise if the expected tables cannot be selected, for instance if a restore hasn't worked as
|
||||
expected.
|
||||
'''
|
||||
command = 'select count(*) from test{id};'
|
||||
|
||||
if 'postgresql_databases' in config:
|
||||
run_postgresql_command(command.format(id=1), config, use_restore_options)
|
||||
if 'mariadb_databases' in config:
|
||||
run_mariadb_command(command.format(id=2), config, use_restore_options)
|
||||
if 'mysql_databases' in config:
|
||||
run_mariadb_command(command.format(id=3), config, use_restore_options, binary_name='mysql')
|
||||
if 'mongodb_databases' in config:
|
||||
assert (
|
||||
get_mongodb_database_client(config, use_restore_options)['test4'].count_documents(
|
||||
filter={}
|
||||
)
|
||||
> 0
|
||||
)
|
||||
if 'sqlite_databases' in config:
|
||||
run_sqlite_command(command.format(id=5), config, use_restore_options)
|
||||
|
||||
|
||||
def test_database_dump_and_restore():
|
||||
@@ -188,15 +356,17 @@ def test_database_dump_and_restore():
|
||||
|
||||
try:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
write_configuration(
|
||||
config = write_configuration(
|
||||
temporary_directory, config_path, repository_path, borgmatic_source_directory
|
||||
)
|
||||
create_test_tables(config)
|
||||
select_test_tables(config)
|
||||
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
|
||||
)
|
||||
|
||||
# Run borgmatic to generate a backup archive including a database dump.
|
||||
# Run borgmatic to generate a backup archive including database dumps.
|
||||
subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
|
||||
|
||||
# Get the created archive name.
|
||||
@@ -209,16 +379,21 @@ def test_database_dump_and_restore():
|
||||
assert len(parsed_output[0]['archives']) == 1
|
||||
archive_name = parsed_output[0]['archives'][0]['archive']
|
||||
|
||||
# Restore the database from the archive.
|
||||
# Restore the databases from the archive.
|
||||
drop_test_tables(config)
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]
|
||||
)
|
||||
|
||||
# Ensure the test tables have actually been restored.
|
||||
select_test_tables(config)
|
||||
finally:
|
||||
os.chdir(original_working_directory)
|
||||
shutil.rmtree(temporary_directory)
|
||||
drop_test_tables(config)
|
||||
|
||||
|
||||
def test_database_dump_and_restore_with_restore_cli_arguments():
|
||||
def test_database_dump_and_restore_with_restore_cli_flags():
|
||||
# Create a Borg repository.
|
||||
temporary_directory = tempfile.mkdtemp()
|
||||
repository_path = os.path.join(temporary_directory, 'test.borg')
|
||||
@@ -228,9 +403,11 @@ def test_database_dump_and_restore_with_restore_cli_arguments():
|
||||
|
||||
try:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
write_simple_custom_restore_configuration(
|
||||
config = write_simple_custom_restore_configuration(
|
||||
temporary_directory, config_path, repository_path, borgmatic_source_directory
|
||||
)
|
||||
create_test_tables(config)
|
||||
select_test_tables(config)
|
||||
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
|
||||
@@ -250,6 +427,7 @@ def test_database_dump_and_restore_with_restore_cli_arguments():
|
||||
archive_name = parsed_output[0]['archives'][0]['archive']
|
||||
|
||||
# Restore the database from the archive.
|
||||
drop_test_tables(config)
|
||||
subprocess.check_call(
|
||||
[
|
||||
'borgmatic',
|
||||
@@ -264,15 +442,25 @@ def test_database_dump_and_restore_with_restore_cli_arguments():
|
||||
'postgresql2',
|
||||
'--port',
|
||||
'5433',
|
||||
'--username',
|
||||
'postgres2',
|
||||
'--password',
|
||||
'test2',
|
||||
]
|
||||
)
|
||||
|
||||
# Ensure the test tables have actually been restored. But first modify the config to contain
|
||||
# the altered restore values from the borgmatic command above. This ensures that the test
|
||||
# tables are selected from the correct database.
|
||||
database = config['postgresql_databases'][0]
|
||||
database['restore_hostname'] = 'postgresql2'
|
||||
database['restore_port'] = '5433'
|
||||
database['restore_password'] = 'test2'
|
||||
|
||||
select_test_tables(config, use_restore_options=True)
|
||||
finally:
|
||||
os.chdir(original_working_directory)
|
||||
shutil.rmtree(temporary_directory)
|
||||
drop_test_tables(config)
|
||||
drop_test_tables(config, use_restore_options=True)
|
||||
|
||||
|
||||
def test_database_dump_and_restore_with_restore_configuration_options():
|
||||
@@ -285,9 +473,11 @@ def test_database_dump_and_restore_with_restore_configuration_options():
|
||||
|
||||
try:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
write_custom_restore_configuration(
|
||||
config = write_custom_restore_configuration(
|
||||
temporary_directory, config_path, repository_path, borgmatic_source_directory
|
||||
)
|
||||
create_test_tables(config)
|
||||
select_test_tables(config)
|
||||
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
|
||||
@@ -307,12 +497,18 @@ def test_database_dump_and_restore_with_restore_configuration_options():
|
||||
archive_name = parsed_output[0]['archives'][0]['archive']
|
||||
|
||||
# Restore the database from the archive.
|
||||
drop_test_tables(config)
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'restore', '--archive', archive_name]
|
||||
)
|
||||
|
||||
# Ensure the test tables have actually been restored.
|
||||
select_test_tables(config, use_restore_options=True)
|
||||
finally:
|
||||
os.chdir(original_working_directory)
|
||||
shutil.rmtree(temporary_directory)
|
||||
drop_test_tables(config)
|
||||
drop_test_tables(config, use_restore_options=True)
|
||||
|
||||
|
||||
def test_database_dump_and_restore_with_directory_format():
|
||||
@@ -325,7 +521,7 @@ def test_database_dump_and_restore_with_directory_format():
|
||||
|
||||
try:
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
write_configuration(
|
||||
config = write_configuration(
|
||||
temporary_directory,
|
||||
config_path,
|
||||
repository_path,
|
||||
@@ -333,6 +529,8 @@ def test_database_dump_and_restore_with_directory_format():
|
||||
postgresql_dump_format='directory',
|
||||
mongodb_dump_format='directory',
|
||||
)
|
||||
create_test_tables(config)
|
||||
select_test_tables(config)
|
||||
|
||||
subprocess.check_call(
|
||||
['borgmatic', '-v', '2', '--config', config_path, 'rcreate', '--encryption', 'repokey']
|
||||
@@ -342,12 +540,17 @@ def test_database_dump_and_restore_with_directory_format():
|
||||
subprocess.check_call(['borgmatic', 'create', '--config', config_path, '-v', '2'])
|
||||
|
||||
# Restore the database from the archive.
|
||||
drop_test_tables(config)
|
||||
subprocess.check_call(
|
||||
['borgmatic', '--config', config_path, 'restore', '--archive', 'latest']
|
||||
)
|
||||
|
||||
# Ensure the test tables have actually been restored.
|
||||
select_test_tables(config)
|
||||
finally:
|
||||
os.chdir(original_working_directory)
|
||||
shutil.rmtree(temporary_directory)
|
||||
drop_test_tables(config)
|
||||
|
||||
|
||||
def test_database_dump_with_error_causes_borgmatic_to_exit():
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_borgmatic_command_with_invalid_flag_shows_error_but_not_traceback():
|
||||
output = subprocess.run(
|
||||
'borgmatic -v 2 --invalid'.split(' '), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
).stdout.decode(sys.stdout.encoding)
|
||||
|
||||
assert 'Unrecognized argument' in output
|
||||
assert 'Traceback' not in output
|
||||
@@ -32,6 +32,9 @@ def assert_command_does_not_duplicate_flags(command, *args, **kwargs):
|
||||
flag_name: 1 for flag_name in flag_counts
|
||||
}, f"Duplicate flags found in: {' '.join(command)}"
|
||||
|
||||
if '--json' in command:
|
||||
return '{}'
|
||||
|
||||
|
||||
def fuzz_argument(arguments, argument_name):
|
||||
'''
|
||||
|
||||
@@ -13,8 +13,9 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||
global_arguments = arguments['global']
|
||||
assert global_arguments.config_paths == config_paths
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == -2
|
||||
assert global_arguments.log_file_verbosity == 1
|
||||
assert global_arguments.monitoring_verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_multiple_config_flags_parses_as_list():
|
||||
@@ -25,8 +26,9 @@ def test_parse_arguments_with_multiple_config_flags_parses_as_list():
|
||||
global_arguments = arguments['global']
|
||||
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == -2
|
||||
assert global_arguments.log_file_verbosity == 1
|
||||
assert global_arguments.monitoring_verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_action_after_config_path_omits_action():
|
||||
@@ -71,8 +73,9 @@ def test_parse_arguments_with_verbosity_overrides_default():
|
||||
global_arguments = arguments['global']
|
||||
assert global_arguments.config_paths == config_paths
|
||||
assert global_arguments.verbosity == 1
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.log_file_verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == -2
|
||||
assert global_arguments.log_file_verbosity == 1
|
||||
assert global_arguments.monitoring_verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||
@@ -85,6 +88,8 @@ def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
||||
assert global_arguments.config_paths == config_paths
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 2
|
||||
assert global_arguments.log_file_verbosity == 1
|
||||
assert global_arguments.monitoring_verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_log_file_verbosity_overrides_default():
|
||||
@@ -96,8 +101,9 @@ def test_parse_arguments_with_log_file_verbosity_overrides_default():
|
||||
global_arguments = arguments['global']
|
||||
assert global_arguments.config_paths == config_paths
|
||||
assert global_arguments.verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == 0
|
||||
assert global_arguments.syslog_verbosity == -2
|
||||
assert global_arguments.log_file_verbosity == -1
|
||||
assert global_arguments.monitoring_verbosity == 1
|
||||
|
||||
|
||||
def test_parse_arguments_with_single_override_parses():
|
||||
@@ -616,3 +622,16 @@ def test_parse_arguments_config_with_subaction_and_explicit_config_file_does_not
|
||||
module.parse_arguments(
|
||||
'config', 'bootstrap', '--repository', 'repo.borg', '--config', 'test.yaml'
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_with_borg_action_and_dry_run_raises():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments('--dry-run', 'borg', 'list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_borg_action_and_no_dry_run_does_not_raise():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('borg', 'list')
|
||||
|
||||
@@ -10,7 +10,7 @@ from borgmatic.config import generate as module
|
||||
|
||||
def test_insert_newline_before_comment_does_not_raise():
|
||||
field_name = 'foo'
|
||||
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Comment')
|
||||
|
||||
module.insert_newline_before_comment(config, field_name)
|
||||
@@ -125,14 +125,16 @@ def test_write_configuration_with_already_existing_directory_does_not_raise():
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_sequence_of_strings_does_not_raise():
|
||||
config = module.yaml.comments.CommentedSeq(['foo', 'bar'])
|
||||
config = module.ruamel.yaml.comments.CommentedSeq(['foo', 'bar'])
|
||||
schema = {'type': 'array', 'items': {'type': 'string'}}
|
||||
|
||||
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')])])
|
||||
config = module.ruamel.yaml.comments.CommentedSeq(
|
||||
[module.ruamel.yaml.comments.CommentedMap([('foo', 'yo')])]
|
||||
)
|
||||
schema = {
|
||||
'type': 'array',
|
||||
'items': {'type': 'object', 'properties': {'foo': {'description': 'yo'}}},
|
||||
@@ -142,7 +144,9 @@ def test_add_comments_to_configuration_sequence_of_maps_does_not_raise():
|
||||
|
||||
|
||||
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')])])
|
||||
config = module.ruamel.yaml.comments.CommentedSeq(
|
||||
[module.ruamel.yaml.comments.CommentedMap([('foo', 'yo')])]
|
||||
)
|
||||
schema = {'type': 'array', 'items': {'type': 'object', 'properties': {'foo': {}}}}
|
||||
|
||||
module.add_comments_to_configuration_sequence(config, schema)
|
||||
@@ -150,7 +154,7 @@ def test_add_comments_to_configuration_sequence_of_maps_without_description_does
|
||||
|
||||
def test_add_comments_to_configuration_object_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)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
|
||||
@@ -160,7 +164,7 @@ def test_add_comments_to_configuration_object_does_not_raise():
|
||||
|
||||
|
||||
def test_add_comments_to_configuration_object_with_skip_first_does_not_raise():
|
||||
config = module.yaml.comments.CommentedMap([('foo', 33)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([('foo', 33)])
|
||||
schema = {'type': 'object', 'properties': {'foo': {'description': 'Foo'}}}
|
||||
|
||||
module.add_comments_to_configuration_object(config, schema, skip_first=True)
|
||||
@@ -168,7 +172,7 @@ def test_add_comments_to_configuration_object_with_skip_first_does_not_raise():
|
||||
|
||||
def test_remove_commented_out_sentinel_keeps_other_comments():
|
||||
field_name = 'foo'
|
||||
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.\nCOMMENT_OUT')
|
||||
|
||||
module.remove_commented_out_sentinel(config, field_name)
|
||||
@@ -180,7 +184,7 @@ def test_remove_commented_out_sentinel_keeps_other_comments():
|
||||
|
||||
def test_remove_commented_out_sentinel_without_sentinel_keeps_other_comments():
|
||||
field_name = 'foo'
|
||||
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||
|
||||
module.remove_commented_out_sentinel(config, field_name)
|
||||
@@ -192,7 +196,7 @@ def test_remove_commented_out_sentinel_without_sentinel_keeps_other_comments():
|
||||
|
||||
def test_remove_commented_out_sentinel_on_unknown_field_does_not_raise():
|
||||
field_name = 'foo'
|
||||
config = module.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
|
||||
config.yaml_set_comment_before_after_key(key=field_name, before='Actual comment.')
|
||||
|
||||
module.remove_commented_out_sentinel(config, 'unknown')
|
||||
@@ -201,7 +205,9 @@ def test_remove_commented_out_sentinel_on_unknown_field_does_not_raise():
|
||||
def test_generate_sample_configuration_does_not_raise():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||
flexmock(module.yaml).should_receive('round_trip_load')
|
||||
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
|
||||
flexmock(load=lambda filename: {})
|
||||
)
|
||||
flexmock(module).should_receive('schema_to_sample_configuration')
|
||||
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||
flexmock(module).should_receive('render_configuration')
|
||||
@@ -214,7 +220,9 @@ def test_generate_sample_configuration_does_not_raise():
|
||||
def test_generate_sample_configuration_with_source_filename_does_not_raise():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||
flexmock(module.yaml).should_receive('round_trip_load')
|
||||
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
|
||||
flexmock(load=lambda filename: {})
|
||||
)
|
||||
flexmock(module.load).should_receive('load_configuration')
|
||||
flexmock(module.normalize).should_receive('normalize')
|
||||
flexmock(module).should_receive('schema_to_sample_configuration')
|
||||
@@ -229,7 +237,9 @@ def test_generate_sample_configuration_with_source_filename_does_not_raise():
|
||||
def test_generate_sample_configuration_with_dry_run_does_not_write_file():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
builtins.should_receive('open').with_args('schema.yaml').and_return('')
|
||||
flexmock(module.yaml).should_receive('round_trip_load')
|
||||
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
|
||||
flexmock(load=lambda filename: {})
|
||||
)
|
||||
flexmock(module).should_receive('schema_to_sample_configuration')
|
||||
flexmock(module).should_receive('merge_source_configuration_into_destination')
|
||||
flexmock(module).should_receive('render_configuration')
|
||||
|
||||
@@ -15,33 +15,12 @@ def test_load_configuration_parses_contents():
|
||||
assert module.load_configuration('config.yaml') == {'key': 'value'}
|
||||
|
||||
|
||||
def test_load_configuration_replaces_constants():
|
||||
def test_load_configuration_with_only_integer_value_does_not_raise():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
config_file = io.StringIO(
|
||||
'''
|
||||
constants:
|
||||
key: value
|
||||
key: {key}
|
||||
'''
|
||||
)
|
||||
config_file = io.StringIO('33')
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
assert module.load_configuration('config.yaml') == {'key': 'value'}
|
||||
|
||||
|
||||
def test_load_configuration_replaces_complex_constants():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
config_file = io.StringIO(
|
||||
'''
|
||||
constants:
|
||||
key:
|
||||
subkey: value
|
||||
key: {key}
|
||||
'''
|
||||
)
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
assert module.load_configuration('config.yaml') == {'key': {'subkey': 'value'}}
|
||||
assert module.load_configuration('config.yaml') == 33
|
||||
|
||||
|
||||
def test_load_configuration_inlines_include_relative_to_current_directory():
|
||||
@@ -124,6 +103,37 @@ def test_load_configuration_raises_if_absolute_include_does_not_exist():
|
||||
assert module.load_configuration('config.yaml')
|
||||
|
||||
|
||||
def test_load_configuration_inlines_multiple_file_include_as_list():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
flexmock(module.os.path).should_receive('isabs').and_return(True)
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
include1_file = io.StringIO('value1')
|
||||
include1_file.name = '/root/include1.yaml'
|
||||
builtins.should_receive('open').with_args('/root/include1.yaml').and_return(include1_file)
|
||||
include2_file = io.StringIO('value2')
|
||||
include2_file.name = '/root/include2.yaml'
|
||||
builtins.should_receive('open').with_args('/root/include2.yaml').and_return(include2_file)
|
||||
config_file = io.StringIO('key: !include [/root/include1.yaml, /root/include2.yaml]')
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
|
||||
assert module.load_configuration('config.yaml') == {'key': ['value2', 'value1']}
|
||||
|
||||
|
||||
def test_load_configuration_include_with_unsupported_filename_type_raises():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
flexmock(module.os.path).should_receive('isabs').and_return(True)
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
config_file = io.StringIO('key: !include {path: /root/include.yaml}')
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.load_configuration('config.yaml')
|
||||
|
||||
|
||||
def test_load_configuration_merges_include():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
@@ -149,6 +159,43 @@ def test_load_configuration_merges_include():
|
||||
assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
|
||||
|
||||
|
||||
def test_load_configuration_merges_multiple_file_include():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
flexmock(module.os.path).should_receive('isabs').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
include1_file = io.StringIO(
|
||||
'''
|
||||
foo: bar
|
||||
baz: quux
|
||||
original: yes
|
||||
'''
|
||||
)
|
||||
include1_file.name = 'include1.yaml'
|
||||
builtins.should_receive('open').with_args('/tmp/include1.yaml').and_return(include1_file)
|
||||
include2_file = io.StringIO(
|
||||
'''
|
||||
baz: second
|
||||
'''
|
||||
)
|
||||
include2_file.name = 'include2.yaml'
|
||||
builtins.should_receive('open').with_args('/tmp/include2.yaml').and_return(include2_file)
|
||||
config_file = io.StringIO(
|
||||
'''
|
||||
foo: override
|
||||
<<: !include [include1.yaml, include2.yaml]
|
||||
'''
|
||||
)
|
||||
config_file.name = 'config.yaml'
|
||||
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
|
||||
|
||||
assert module.load_configuration('config.yaml') == {
|
||||
'foo': 'override',
|
||||
'baz': 'second',
|
||||
'original': 'yes',
|
||||
}
|
||||
|
||||
|
||||
def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
@@ -438,8 +485,9 @@ def test_raise_omit_node_error_raises():
|
||||
module.raise_omit_node_error(loader=flexmock(), node=flexmock())
|
||||
|
||||
|
||||
def test_filter_omitted_nodes():
|
||||
nodes = [
|
||||
def test_filter_omitted_nodes_discards_values_with_omit_tag_and_also_equal_values():
|
||||
nodes = [flexmock(), flexmock()]
|
||||
values = [
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='b'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
|
||||
@@ -448,11 +496,139 @@ def test_filter_omitted_nodes():
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
|
||||
]
|
||||
|
||||
result = module.filter_omitted_nodes(nodes)
|
||||
result = module.filter_omitted_nodes(nodes, values)
|
||||
|
||||
assert [item.value for item in result] == ['a', 'c', 'a', 'c']
|
||||
|
||||
|
||||
def test_filter_omitted_nodes_keeps_all_values_when_given_only_one_node():
|
||||
nodes = [flexmock()]
|
||||
values = [
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='b'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='a'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='!omit', value='b'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='c'),
|
||||
]
|
||||
|
||||
result = module.filter_omitted_nodes(nodes, values)
|
||||
|
||||
assert [item.value for item in result] == ['a', 'b', 'c', 'a', 'b', 'c']
|
||||
|
||||
|
||||
def test_merge_values_combines_mapping_values():
|
||||
nodes = [
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.MappingNode(
|
||||
tag='tag:yaml.org,2002:map',
|
||||
value=[
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:str', value='keep_hourly'
|
||||
),
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:int', value='24'
|
||||
),
|
||||
),
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:str', value='keep_daily'
|
||||
),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.MappingNode(
|
||||
tag='tag:yaml.org,2002:map',
|
||||
value=[
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:str', value='keep_daily'
|
||||
),
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:int', value='25'
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.MappingNode(
|
||||
tag='tag:yaml.org,2002:map',
|
||||
value=[
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:str', value='keep_nanosecondly'
|
||||
),
|
||||
module.ruamel.yaml.nodes.ScalarNode(
|
||||
tag='tag:yaml.org,2002:int', value='1000'
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
values = module.merge_values(nodes)
|
||||
|
||||
assert len(values) == 4
|
||||
assert values[0][0].value == 'keep_hourly'
|
||||
assert values[0][1].value == '24'
|
||||
assert values[1][0].value == 'keep_daily'
|
||||
assert values[1][1].value == '7'
|
||||
assert values[2][0].value == 'keep_daily'
|
||||
assert values[2][1].value == '25'
|
||||
assert values[3][0].value == 'keep_nanosecondly'
|
||||
assert values[3][1].value == '1000'
|
||||
|
||||
|
||||
def test_merge_values_combines_sequence_values():
|
||||
nodes = [
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.SequenceNode(
|
||||
tag='tag:yaml.org,2002:seq',
|
||||
value=[
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='1'),
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='2'),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.SequenceNode(
|
||||
tag='tag:yaml.org,2002:seq',
|
||||
value=[
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='3'),
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='option'),
|
||||
module.ruamel.yaml.nodes.SequenceNode(
|
||||
tag='tag:yaml.org,2002:seq',
|
||||
value=[
|
||||
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='4'),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
values = module.merge_values(nodes)
|
||||
|
||||
assert len(values) == 4
|
||||
assert values[0].value == '1'
|
||||
assert values[1].value == '2'
|
||||
assert values[2].value == '3'
|
||||
assert values[3].value == '4'
|
||||
|
||||
|
||||
def test_deep_merge_nodes_replaces_colliding_scalar_values():
|
||||
node_values = [
|
||||
(
|
||||
@@ -499,10 +675,10 @@ def test_deep_merge_nodes_replaces_colliding_scalar_values():
|
||||
assert section_key.value == 'retention'
|
||||
options = section_value.value
|
||||
assert len(options) == 2
|
||||
assert options[0][0].value == 'keep_hourly'
|
||||
assert options[0][1].value == '24'
|
||||
assert options[1][0].value == 'keep_daily'
|
||||
assert options[1][1].value == '5'
|
||||
assert options[0][0].value == 'keep_daily'
|
||||
assert options[0][1].value == '5'
|
||||
assert options[1][0].value == 'keep_hourly'
|
||||
assert options[1][1].value == '24'
|
||||
|
||||
|
||||
def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
|
||||
@@ -553,10 +729,10 @@ def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
|
||||
assert section_key.value == 'retention'
|
||||
options = section_value.value
|
||||
assert len(options) == 3
|
||||
assert options[0][0].value == 'keep_hourly'
|
||||
assert options[0][1].value == '24'
|
||||
assert options[1][0].value == 'keep_daily'
|
||||
assert options[1][1].value == '7'
|
||||
assert options[0][0].value == 'keep_daily'
|
||||
assert options[0][1].value == '7'
|
||||
assert options[1][0].value == 'keep_hourly'
|
||||
assert options[1][1].value == '24'
|
||||
assert options[2][0].value == 'keep_minutely'
|
||||
assert options[2][1].value == '10'
|
||||
|
||||
@@ -629,10 +805,10 @@ def test_deep_merge_nodes_keeps_deeply_nested_values():
|
||||
assert section_key.value == 'storage'
|
||||
options = section_value.value
|
||||
assert len(options) == 2
|
||||
assert options[0][0].value == 'lock_wait'
|
||||
assert options[0][1].value == '5'
|
||||
assert options[1][0].value == 'extra_borg_options'
|
||||
nested_options = options[1][1].value
|
||||
assert options[0][0].value == 'extra_borg_options'
|
||||
assert options[1][0].value == 'lock_wait'
|
||||
assert options[1][1].value == '5'
|
||||
nested_options = options[0][1].value
|
||||
assert len(nested_options) == 2
|
||||
assert nested_options[0][0].value == 'init'
|
||||
assert nested_options[0][1].value == '--init-option'
|
||||
|
||||
@@ -4,19 +4,24 @@ from borgmatic.config import override as module
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,expected_result',
|
||||
'value,expected_result,option_type',
|
||||
(
|
||||
('thing', 'thing'),
|
||||
('33', 33),
|
||||
('33b', '33b'),
|
||||
('true', True),
|
||||
('false', False),
|
||||
('[foo]', ['foo']),
|
||||
('[foo, bar]', ['foo', 'bar']),
|
||||
('thing', 'thing', 'string'),
|
||||
('33', 33, 'integer'),
|
||||
('33', '33', 'string'),
|
||||
('33b', '33b', 'integer'),
|
||||
('33b', '33b', 'string'),
|
||||
('true', True, 'boolean'),
|
||||
('false', False, 'boolean'),
|
||||
('true', 'true', 'string'),
|
||||
('[foo]', ['foo'], 'array'),
|
||||
('[foo]', '[foo]', 'string'),
|
||||
('[foo, bar]', ['foo', 'bar'], 'array'),
|
||||
('[foo, bar]', '[foo, bar]', 'string'),
|
||||
),
|
||||
)
|
||||
def test_convert_value_type_coerces_values(value, expected_result):
|
||||
assert module.convert_value_type(value) == expected_result
|
||||
def test_convert_value_type_coerces_values(value, expected_result, option_type):
|
||||
assert module.convert_value_type(value, option_type) == expected_result
|
||||
|
||||
|
||||
def test_apply_overrides_updates_config():
|
||||
@@ -25,16 +30,23 @@ def test_apply_overrides_updates_config():
|
||||
'other_section.thing=value2',
|
||||
'section.nested.key=value3',
|
||||
'new.foo=bar',
|
||||
'new.mylist=[baz]',
|
||||
'new.nonlist=[quux]',
|
||||
]
|
||||
config = {
|
||||
'section': {'key': 'value', 'other': 'other_value'},
|
||||
'other_section': {'thing': 'thing_value'},
|
||||
}
|
||||
schema = {
|
||||
'properties': {
|
||||
'new': {'properties': {'mylist': {'type': 'array'}, 'nonlist': {'type': 'string'}}}
|
||||
}
|
||||
}
|
||||
|
||||
module.apply_overrides(config, raw_overrides)
|
||||
module.apply_overrides(config, schema, raw_overrides)
|
||||
|
||||
assert config == {
|
||||
'section': {'key': 'value1', 'other': 'other_value', 'nested': {'key': 'value3'}},
|
||||
'other_section': {'thing': 'value2'},
|
||||
'new': {'foo': 'bar'},
|
||||
'new': {'foo': 'bar', 'mylist': ['baz'], 'nonlist': '[quux]'},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import io
|
||||
import os
|
||||
import string
|
||||
import sys
|
||||
|
||||
@@ -117,7 +118,7 @@ def test_parse_configuration_with_schema_lacking_examples_does_not_raise():
|
||||
module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
|
||||
|
||||
|
||||
def test_parse_configuration_inlines_include():
|
||||
def test_parse_configuration_inlines_include_inside_deprecated_section():
|
||||
mock_config_and_schema(
|
||||
'''
|
||||
source_directories:
|
||||
@@ -244,7 +245,7 @@ def test_parse_configuration_applies_overrides():
|
||||
assert logs == []
|
||||
|
||||
|
||||
def test_parse_configuration_applies_normalization():
|
||||
def test_parse_configuration_applies_normalization_after_environment_variable_interpolation():
|
||||
mock_config_and_schema(
|
||||
'''
|
||||
location:
|
||||
@@ -252,17 +253,18 @@ def test_parse_configuration_applies_normalization():
|
||||
- /home
|
||||
|
||||
repositories:
|
||||
- path: hostname.borg
|
||||
- ${NO_EXIST:-user@hostname:repo}
|
||||
|
||||
exclude_if_present: .nobackup
|
||||
'''
|
||||
)
|
||||
flexmock(os).should_receive('getenv').replace_with(lambda variable_name, default: default)
|
||||
|
||||
config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
|
||||
|
||||
assert config == {
|
||||
'source_directories': ['/home'],
|
||||
'repositories': [{'path': 'hostname.borg'}],
|
||||
'repositories': [{'path': 'ssh://user@hostname/./repo'}],
|
||||
'exclude_if_present': ['.nobackup'],
|
||||
}
|
||||
assert logs
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import logging
|
||||
import platform
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import loki as module
|
||||
|
||||
|
||||
def test_initialize_monitor_replaces_labels():
|
||||
'''
|
||||
Assert that label placeholders get replaced.
|
||||
'''
|
||||
hook_config = {
|
||||
'url': 'http://localhost:3100/loki/api/v1/push',
|
||||
'labels': {'hostname': '__hostname', 'config': '__config', 'config_full': '__config_path'},
|
||||
}
|
||||
config_filename = '/mock/path/test.yaml'
|
||||
dry_run = True
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
assert handler.buffer.root['streams'][0]['stream']['hostname'] == platform.node()
|
||||
assert handler.buffer.root['streams'][0]['stream']['config'] == 'test.yaml'
|
||||
assert handler.buffer.root['streams'][0]['stream']['config_full'] == config_filename
|
||||
return
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
def test_initialize_monitor_adds_log_handler():
|
||||
'''
|
||||
Assert that calling initialize_monitor adds our logger to the root logger.
|
||||
'''
|
||||
hook_config = {'url': 'http://localhost:3100/loki/api/v1/push', 'labels': {'app': 'borgmatic'}}
|
||||
module.initialize_monitor(
|
||||
hook_config,
|
||||
flexmock(),
|
||||
config_filename='test.yaml',
|
||||
monitoring_log_level=flexmock(),
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
return
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
def test_ping_monitor_adds_log_message():
|
||||
'''
|
||||
Assert that calling ping_monitor adds a message to our logger.
|
||||
'''
|
||||
hook_config = {'url': 'http://localhost:3100/loki/api/v1/push', 'labels': {'app': 'borgmatic'}}
|
||||
config_filename = 'test.yaml'
|
||||
dry_run = True
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run)
|
||||
module.ping_monitor(
|
||||
hook_config, flexmock(), config_filename, module.monitor.State.FINISH, flexmock(), dry_run
|
||||
)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
assert any(
|
||||
map(
|
||||
lambda log: log
|
||||
== f'{config_filename}: {module.MONITOR_STATE_TO_LOKI[module.monitor.State.FINISH]} backup',
|
||||
map(lambda x: x[1], handler.buffer.root['streams'][0]['values']),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
def test_destroy_monitor_removes_log_handler():
|
||||
'''
|
||||
Assert that destroy_monitor removes the logger from the root logger.
|
||||
'''
|
||||
hook_config = {'url': 'http://localhost:3100/loki/api/v1/push', 'labels': {'app': 'borgmatic'}}
|
||||
config_filename = 'test.yaml'
|
||||
dry_run = True
|
||||
module.initialize_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run)
|
||||
module.destroy_monitor(hook_config, flexmock(), config_filename, flexmock(), dry_run)
|
||||
|
||||
for handler in tuple(logging.getLogger().handlers):
|
||||
if isinstance(handler, module.Loki_log_handler):
|
||||
assert False
|
||||
@@ -40,6 +40,41 @@ def test_run_create_executes_and_calls_hooks_for_configured_repository():
|
||||
)
|
||||
|
||||
|
||||
def test_run_create_with_store_config_files_false_does_not_create_borgmatic_manifest():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').never()
|
||||
flexmock(module.borgmatic.borg.create).should_receive('create_archive').once()
|
||||
flexmock(module).should_receive('create_borgmatic_manifest').never()
|
||||
flexmock(module.borgmatic.hooks.command).should_receive('execute_hook').times(2)
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hooks').and_return({})
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).and_return({})
|
||||
create_arguments = flexmock(
|
||||
repository=None,
|
||||
progress=flexmock(),
|
||||
stats=flexmock(),
|
||||
json=flexmock(),
|
||||
list_files=flexmock(),
|
||||
)
|
||||
global_arguments = flexmock(monitoring_verbosity=1, dry_run=False, used_config_paths=[])
|
||||
|
||||
list(
|
||||
module.run_create(
|
||||
config_filename='test.yaml',
|
||||
repository={'path': 'repo'},
|
||||
config={'store_config_files': False},
|
||||
hook_context={},
|
||||
local_borg_version=None,
|
||||
create_arguments=create_arguments,
|
||||
global_arguments=global_arguments,
|
||||
dry_run_label='',
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_create_runs_with_selected_repository():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive(
|
||||
@@ -116,7 +151,7 @@ def test_create_borgmatic_manifest_creates_manifest_file():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os).should_receive('makedirs').and_return(True)
|
||||
|
||||
flexmock(module.importlib_metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(sys.modules['builtins']).should_receive('open').with_args(
|
||||
'/home/user/.borgmatic/bootstrap/manifest.json', 'w'
|
||||
).and_return(
|
||||
@@ -137,7 +172,7 @@ def test_create_borgmatic_manifest_creates_manifest_file_with_custom_borgmatic_s
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.os).should_receive('makedirs').and_return(True)
|
||||
|
||||
flexmock(module.importlib_metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(module.importlib.metadata).should_receive('version').and_return('1.0.0')
|
||||
flexmock(sys.modules['builtins']).should_receive('open').with_args(
|
||||
'/borgmatic/bootstrap/manifest.json', 'w'
|
||||
).and_return(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.actions import export_key as module
|
||||
|
||||
|
||||
def test_run_export_key_does_not_raise():
|
||||
flexmock(module.logger).answer = lambda message: None
|
||||
flexmock(module.borgmatic.config.validate).should_receive('repositories_match').and_return(True)
|
||||
flexmock(module.borgmatic.borg.export_key).should_receive('export_key')
|
||||
export_arguments = flexmock(repository=flexmock())
|
||||
|
||||
module.run_export_key(
|
||||
repository={'path': 'repo'},
|
||||
config={},
|
||||
local_borg_version=None,
|
||||
export_arguments=export_arguments,
|
||||
global_arguments=flexmock(),
|
||||
local_path=None,
|
||||
remote_path=None,
|
||||
)
|
||||
+150
-135
@@ -4,60 +4,71 @@ from flexmock import flexmock
|
||||
import borgmatic.actions.restore as module
|
||||
|
||||
|
||||
def test_get_configured_database_matches_database_by_name():
|
||||
assert module.get_configured_database(
|
||||
def test_get_configured_data_source_matches_data_source_by_name():
|
||||
assert module.get_configured_data_source(
|
||||
config={
|
||||
'other_databases': [{'name': 'other'}],
|
||||
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
||||
},
|
||||
archive_database_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||
archive_data_source_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||
hook_name='postgresql_databases',
|
||||
database_name='bar',
|
||||
data_source_name='bar',
|
||||
) == ('postgresql_databases', {'name': 'bar'})
|
||||
|
||||
|
||||
def test_get_configured_database_matches_nothing_when_database_name_not_configured():
|
||||
assert module.get_configured_database(
|
||||
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||
archive_database_names={'postgresql_databases': ['foo']},
|
||||
def test_get_configured_data_source_matches_nothing_when_nothing_configured():
|
||||
assert module.get_configured_data_source(
|
||||
config={},
|
||||
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||
hook_name='postgresql_databases',
|
||||
database_name='quux',
|
||||
data_source_name='quux',
|
||||
) == (None, None)
|
||||
|
||||
|
||||
def test_get_configured_database_matches_nothing_when_database_name_not_in_archive():
|
||||
assert module.get_configured_database(
|
||||
def test_get_configured_data_source_matches_nothing_when_data_source_name_not_configured():
|
||||
assert module.get_configured_data_source(
|
||||
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||
archive_database_names={'postgresql_databases': ['bar']},
|
||||
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
data_source_name='quux',
|
||||
) == (None, None)
|
||||
|
||||
|
||||
def test_get_configured_database_matches_database_by_configuration_database_name():
|
||||
assert module.get_configured_database(
|
||||
def test_get_configured_data_source_matches_nothing_when_data_source_name_not_in_archive():
|
||||
assert module.get_configured_data_source(
|
||||
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||
archive_data_source_names={'postgresql_databases': ['bar']},
|
||||
hook_name='postgresql_databases',
|
||||
data_source_name='foo',
|
||||
) == (None, None)
|
||||
|
||||
|
||||
def test_get_configured_data_source_matches_data_source_by_configuration_data_source_name():
|
||||
assert module.get_configured_data_source(
|
||||
config={'postgresql_databases': [{'name': 'all'}, {'name': 'bar'}]},
|
||||
archive_database_names={'postgresql_databases': ['foo']},
|
||||
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
configuration_database_name='all',
|
||||
data_source_name='foo',
|
||||
configuration_data_source_name='all',
|
||||
) == ('postgresql_databases', {'name': 'all'})
|
||||
|
||||
|
||||
def test_get_configured_database_with_unspecified_hook_matches_database_by_name():
|
||||
assert module.get_configured_database(
|
||||
def test_get_configured_data_source_with_unspecified_hook_matches_data_source_by_name():
|
||||
assert module.get_configured_data_source(
|
||||
config={
|
||||
'other_databases': [{'name': 'other'}],
|
||||
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
||||
},
|
||||
archive_database_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||
archive_data_source_names={'postgresql_databases': ['other', 'foo', 'bar']},
|
||||
hook_name=module.UNSPECIFIED_HOOK,
|
||||
database_name='bar',
|
||||
data_source_name='bar',
|
||||
) == ('postgresql_databases', {'name': 'bar'})
|
||||
|
||||
|
||||
def test_collect_archive_database_names_parses_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||
def test_collect_archive_data_source_names_parses_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
[
|
||||
'.borgmatic/postgresql_databases/localhost/foo',
|
||||
@@ -66,7 +77,7 @@ def test_collect_archive_database_names_parses_archive_paths():
|
||||
]
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
archive_data_source_names = module.collect_archive_data_source_names(
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
config={'borgmatic_source_directory': '.borgmatic'},
|
||||
@@ -76,14 +87,16 @@ def test_collect_archive_database_names_parses_archive_paths():
|
||||
remote_path=flexmock(),
|
||||
)
|
||||
|
||||
assert archive_database_names == {
|
||||
assert archive_data_source_names == {
|
||||
'postgresql_databases': ['foo', 'bar'],
|
||||
'mysql_databases': ['quux'],
|
||||
}
|
||||
|
||||
|
||||
def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||
def test_collect_archive_data_source_names_parses_directory_format_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
[
|
||||
'.borgmatic/postgresql_databases/localhost/foo/table1',
|
||||
@@ -91,7 +104,7 @@ def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
||||
]
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
archive_data_source_names = module.collect_archive_data_source_names(
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
config={'borgmatic_source_directory': '.borgmatic'},
|
||||
@@ -101,18 +114,20 @@ def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
||||
remote_path=flexmock(),
|
||||
)
|
||||
|
||||
assert archive_database_names == {
|
||||
assert archive_data_source_names == {
|
||||
'postgresql_databases': ['foo'],
|
||||
}
|
||||
|
||||
|
||||
def test_collect_archive_database_names_skips_bad_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
||||
def test_collect_archive_data_source_names_skips_bad_archive_paths():
|
||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_data_source_dump_path').and_return(
|
||||
''
|
||||
)
|
||||
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||
['.borgmatic/postgresql_databases/localhost/foo', '.borgmatic/invalid', 'invalid/as/well']
|
||||
)
|
||||
|
||||
archive_database_names = module.collect_archive_database_names(
|
||||
archive_data_source_names = module.collect_archive_data_source_names(
|
||||
repository={'path': 'repo'},
|
||||
archive='archive',
|
||||
config={'borgmatic_source_directory': '.borgmatic'},
|
||||
@@ -122,96 +137,96 @@ def test_collect_archive_database_names_skips_bad_archive_paths():
|
||||
remote_path=flexmock(),
|
||||
)
|
||||
|
||||
assert archive_database_names == {
|
||||
assert archive_data_source_names == {
|
||||
'postgresql_databases': ['foo'],
|
||||
}
|
||||
|
||||
|
||||
def test_find_databases_to_restore_passes_through_requested_names_found_in_archive():
|
||||
restore_names = module.find_databases_to_restore(
|
||||
requested_database_names=['foo', 'bar'],
|
||||
archive_database_names={'postresql_databases': ['foo', 'bar', 'baz']},
|
||||
def test_find_data_sources_to_restore_passes_through_requested_names_found_in_archive():
|
||||
restore_names = module.find_data_sources_to_restore(
|
||||
requested_data_source_names=['foo', 'bar'],
|
||||
archive_data_source_names={'postresql_databases': ['foo', 'bar', 'baz']},
|
||||
)
|
||||
|
||||
assert restore_names == {module.UNSPECIFIED_HOOK: ['foo', 'bar']}
|
||||
|
||||
|
||||
def test_find_databases_to_restore_raises_for_requested_names_missing_from_archive():
|
||||
def test_find_data_sources_to_restore_raises_for_requested_names_missing_from_archive():
|
||||
with pytest.raises(ValueError):
|
||||
module.find_databases_to_restore(
|
||||
requested_database_names=['foo', 'bar'],
|
||||
archive_database_names={'postresql_databases': ['foo']},
|
||||
module.find_data_sources_to_restore(
|
||||
requested_data_source_names=['foo', 'bar'],
|
||||
archive_data_source_names={'postresql_databases': ['foo']},
|
||||
)
|
||||
|
||||
|
||||
def test_find_databases_to_restore_without_requested_names_finds_all_archive_databases():
|
||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||
def test_find_data_sources_to_restore_without_requested_names_finds_all_archive_data_sources():
|
||||
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||
|
||||
restore_names = module.find_databases_to_restore(
|
||||
requested_database_names=[],
|
||||
archive_database_names=archive_database_names,
|
||||
restore_names = module.find_data_sources_to_restore(
|
||||
requested_data_source_names=[],
|
||||
archive_data_source_names=archive_data_source_names,
|
||||
)
|
||||
|
||||
assert restore_names == archive_database_names
|
||||
assert restore_names == archive_data_source_names
|
||||
|
||||
|
||||
def test_find_databases_to_restore_with_all_in_requested_names_finds_all_archive_databases():
|
||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||
def test_find_data_sources_to_restore_with_all_in_requested_names_finds_all_archive_data_sources():
|
||||
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||
|
||||
restore_names = module.find_databases_to_restore(
|
||||
requested_database_names=['all'],
|
||||
archive_database_names=archive_database_names,
|
||||
restore_names = module.find_data_sources_to_restore(
|
||||
requested_data_source_names=['all'],
|
||||
archive_data_source_names=archive_data_source_names,
|
||||
)
|
||||
|
||||
assert restore_names == archive_database_names
|
||||
assert restore_names == archive_data_source_names
|
||||
|
||||
|
||||
def test_find_databases_to_restore_with_all_in_requested_names_plus_additional_requested_names_omits_duplicates():
|
||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
||||
def test_find_data_sources_to_restore_with_all_in_requested_names_plus_additional_requested_names_omits_duplicates():
|
||||
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||
|
||||
restore_names = module.find_databases_to_restore(
|
||||
requested_database_names=['all', 'foo', 'bar'],
|
||||
archive_database_names=archive_database_names,
|
||||
restore_names = module.find_data_sources_to_restore(
|
||||
requested_data_source_names=['all', 'foo', 'bar'],
|
||||
archive_data_source_names=archive_data_source_names,
|
||||
)
|
||||
|
||||
assert restore_names == archive_database_names
|
||||
assert restore_names == archive_data_source_names
|
||||
|
||||
|
||||
def test_find_databases_to_restore_raises_for_all_in_requested_names_and_requested_named_missing_from_archives():
|
||||
def test_find_data_sources_to_restore_raises_for_all_in_requested_names_and_requested_named_missing_from_archives():
|
||||
with pytest.raises(ValueError):
|
||||
module.find_databases_to_restore(
|
||||
requested_database_names=['all', 'foo', 'bar'],
|
||||
archive_database_names={'postresql_databases': ['foo']},
|
||||
module.find_data_sources_to_restore(
|
||||
requested_data_source_names=['all', 'foo', 'bar'],
|
||||
archive_data_source_names={'postresql_databases': ['foo']},
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_databases_found_with_all_databases_found_does_not_raise():
|
||||
module.ensure_databases_found(
|
||||
def test_ensure_data_sources_found_with_all_data_sources_found_does_not_raise():
|
||||
module.ensure_data_sources_found(
|
||||
restore_names={'postgresql_databases': ['foo']},
|
||||
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||
found_names=['foo', 'bar'],
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_databases_found_with_no_databases_raises():
|
||||
def test_ensure_data_sources_found_with_no_data_sources_raises():
|
||||
with pytest.raises(ValueError):
|
||||
module.ensure_databases_found(
|
||||
module.ensure_data_sources_found(
|
||||
restore_names={'postgresql_databases': []},
|
||||
remaining_restore_names={},
|
||||
found_names=[],
|
||||
)
|
||||
|
||||
|
||||
def test_ensure_databases_found_with_missing_databases_raises():
|
||||
def test_ensure_data_sources_found_with_missing_data_sources_raises():
|
||||
with pytest.raises(ValueError):
|
||||
module.ensure_databases_found(
|
||||
module.ensure_data_sources_found(
|
||||
restore_names={'postgresql_databases': ['foo']},
|
||||
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||
found_names=['foo'],
|
||||
)
|
||||
|
||||
|
||||
def test_run_restore_restores_each_database():
|
||||
def test_run_restore_restores_each_data_source():
|
||||
restore_names = {
|
||||
'postgresql_databases': ['foo', 'bar'],
|
||||
}
|
||||
@@ -221,12 +236,12 @@ def test_run_restore_restores_each_database():
|
||||
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_database').and_return(
|
||||
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_data_source').and_return(
|
||||
('postgresql_databases', {'name': 'foo'})
|
||||
).and_return(('postgresql_databases', {'name': 'bar'}))
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -235,10 +250,10 @@ def test_run_restore_restores_each_database():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'foo', 'schemas': None},
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -247,10 +262,10 @@ def test_run_restore_restores_each_database():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'bar', 'schemas': None},
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
module.run_restore(
|
||||
repository={'path': 'repo'},
|
||||
@@ -259,7 +274,7 @@ def test_run_restore_restores_each_database():
|
||||
restore_arguments=flexmock(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
databases=flexmock(),
|
||||
data_sources=flexmock(),
|
||||
schemas=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
@@ -280,20 +295,20 @@ def test_run_restore_bails_for_non_matching_repository():
|
||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||
'call_hooks_even_if_unconfigured'
|
||||
).never()
|
||||
flexmock(module).should_receive('restore_single_database').never()
|
||||
flexmock(module).should_receive('restore_single_data_source').never()
|
||||
|
||||
module.run_restore(
|
||||
repository={'path': 'repo'},
|
||||
config=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
restore_arguments=flexmock(repository='repo', archive='archive', databases=flexmock()),
|
||||
restore_arguments=flexmock(repository='repo', archive='archive', data_sources=flexmock()),
|
||||
global_arguments=flexmock(dry_run=False),
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
)
|
||||
|
||||
|
||||
def test_run_restore_restores_database_configured_with_all_name():
|
||||
def test_run_restore_restores_data_source_configured_with_all_name():
|
||||
restore_names = {
|
||||
'postgresql_databases': ['foo', 'bar'],
|
||||
}
|
||||
@@ -303,28 +318,28 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
data_source_name='foo',
|
||||
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='bar',
|
||||
data_source_name='bar',
|
||||
).and_return((None, None))
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='bar',
|
||||
configuration_database_name='all',
|
||||
data_source_name='bar',
|
||||
configuration_data_source_name='all',
|
||||
).and_return(('postgresql_databases', {'name': 'bar'}))
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -333,10 +348,10 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'foo', 'schemas': None},
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -345,10 +360,10 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'bar', 'schemas': None},
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
module.run_restore(
|
||||
repository={'path': 'repo'},
|
||||
@@ -357,7 +372,7 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
restore_arguments=flexmock(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
databases=flexmock(),
|
||||
data_sources=flexmock(),
|
||||
schemas=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
@@ -371,7 +386,7 @@ def test_run_restore_restores_database_configured_with_all_name():
|
||||
)
|
||||
|
||||
|
||||
def test_run_restore_skips_missing_database():
|
||||
def test_run_restore_skips_missing_data_source():
|
||||
restore_names = {
|
||||
'postgresql_databases': ['foo', 'bar'],
|
||||
}
|
||||
@@ -381,28 +396,28 @@ def test_run_restore_skips_missing_database():
|
||||
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
data_source_name='foo',
|
||||
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='bar',
|
||||
data_source_name='bar',
|
||||
).and_return((None, None))
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='bar',
|
||||
configuration_database_name='all',
|
||||
data_source_name='bar',
|
||||
configuration_data_source_name='all',
|
||||
).and_return((None, None))
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -411,10 +426,10 @@ def test_run_restore_skips_missing_database():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'foo', 'schemas': None},
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -423,10 +438,10 @@ def test_run_restore_skips_missing_database():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'bar', 'schemas': None},
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
).never()
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
module.run_restore(
|
||||
repository={'path': 'repo'},
|
||||
@@ -435,7 +450,7 @@ def test_run_restore_skips_missing_database():
|
||||
restore_arguments=flexmock(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
databases=flexmock(),
|
||||
data_sources=flexmock(),
|
||||
schemas=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
@@ -449,7 +464,7 @@ def test_run_restore_skips_missing_database():
|
||||
)
|
||||
|
||||
|
||||
def test_run_restore_restores_databases_from_different_hooks():
|
||||
def test_run_restore_restores_data_sources_from_different_hooks():
|
||||
restore_names = {
|
||||
'postgresql_databases': ['foo'],
|
||||
'mysql_databases': ['bar'],
|
||||
@@ -460,21 +475,21 @@ def test_run_restore_restores_databases_from_different_hooks():
|
||||
flexmock(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||
flexmock()
|
||||
)
|
||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
data_source_name='foo',
|
||||
).and_return(('postgresql_databases', {'name': 'foo'}))
|
||||
flexmock(module).should_receive('get_configured_database').with_args(
|
||||
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||
config=object,
|
||||
archive_database_names=object,
|
||||
archive_data_source_names=object,
|
||||
hook_name='mysql_databases',
|
||||
database_name='bar',
|
||||
data_source_name='bar',
|
||||
).and_return(('mysql_databases', {'name': 'bar'}))
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -483,10 +498,10 @@ def test_run_restore_restores_databases_from_different_hooks():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='postgresql_databases',
|
||||
database={'name': 'foo', 'schemas': None},
|
||||
data_source={'name': 'foo', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('restore_single_database').with_args(
|
||||
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||
repository=object,
|
||||
config=object,
|
||||
local_borg_version=object,
|
||||
@@ -495,10 +510,10 @@ def test_run_restore_restores_databases_from_different_hooks():
|
||||
remote_path=object,
|
||||
archive_name=object,
|
||||
hook_name='mysql_databases',
|
||||
database={'name': 'bar', 'schemas': None},
|
||||
data_source={'name': 'bar', 'schemas': None},
|
||||
connection_params=object,
|
||||
).once()
|
||||
flexmock(module).should_receive('ensure_databases_found')
|
||||
flexmock(module).should_receive('ensure_data_sources_found')
|
||||
|
||||
module.run_restore(
|
||||
repository={'path': 'repo'},
|
||||
@@ -507,7 +522,7 @@ def test_run_restore_restores_databases_from_different_hooks():
|
||||
restore_arguments=flexmock(
|
||||
repository='repo',
|
||||
archive='archive',
|
||||
databases=flexmock(),
|
||||
data_sources=flexmock(),
|
||||
schemas=None,
|
||||
hostname=None,
|
||||
port=None,
|
||||
|
||||
+160
-20
@@ -193,6 +193,19 @@ def test_filter_checks_on_frequency_restains_check_with_unelapsed_frequency_and_
|
||||
) == ('archives',)
|
||||
|
||||
|
||||
def test_filter_checks_on_frequency_passes_through_empty_checks():
|
||||
assert (
|
||||
module.filter_checks_on_frequency(
|
||||
config={'checks': [{'name': 'archives', 'frequency': '1 hour'}]},
|
||||
borg_repository_id='repo',
|
||||
checks=(),
|
||||
force=False,
|
||||
archives_check_id='1234',
|
||||
)
|
||||
== ()
|
||||
)
|
||||
|
||||
|
||||
def test_make_archive_filter_flags_with_default_checks_and_prefix_returns_default_flags():
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
@@ -201,6 +214,7 @@ def test_make_archive_filter_flags_with_default_checks_and_prefix_returns_defaul
|
||||
'1.2.3',
|
||||
{},
|
||||
('repository', 'archives'),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
prefix='foo',
|
||||
)
|
||||
|
||||
@@ -215,6 +229,7 @@ def test_make_archive_filter_flags_with_all_checks_and_prefix_returns_default_fl
|
||||
'1.2.3',
|
||||
{},
|
||||
('repository', 'archives', 'extract'),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
prefix='foo',
|
||||
)
|
||||
|
||||
@@ -229,6 +244,7 @@ def test_make_archive_filter_flags_with_all_checks_and_prefix_without_borg_featu
|
||||
'1.2.3',
|
||||
{},
|
||||
('repository', 'archives', 'extract'),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
prefix='foo',
|
||||
)
|
||||
|
||||
@@ -239,7 +255,9 @@ def test_make_archive_filter_flags_with_archives_check_and_last_includes_last_fl
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('archives',), check_last=3)
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('archives',), check_arguments=flexmock(match_archives=None), check_last=3
|
||||
)
|
||||
|
||||
assert flags == ('--last', '3')
|
||||
|
||||
@@ -248,7 +266,9 @@ def test_make_archive_filter_flags_with_data_check_and_last_includes_last_flag()
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('data',), check_last=3)
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('data',), check_arguments=flexmock(match_archives=None), check_last=3
|
||||
)
|
||||
|
||||
assert flags == ('--last', '3')
|
||||
|
||||
@@ -257,7 +277,9 @@ def test_make_archive_filter_flags_with_repository_check_and_last_omits_last_fla
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('repository',), check_last=3)
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('repository',), check_arguments=flexmock(match_archives=None), check_last=3
|
||||
)
|
||||
|
||||
assert flags == ()
|
||||
|
||||
@@ -266,7 +288,13 @@ def test_make_archive_filter_flags_with_default_checks_and_last_includes_last_fl
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('repository', 'archives'), check_last=3)
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3',
|
||||
{},
|
||||
('repository', 'archives'),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
check_last=3,
|
||||
)
|
||||
|
||||
assert flags == ('--last', '3')
|
||||
|
||||
@@ -275,7 +303,9 @@ def test_make_archive_filter_flags_with_archives_check_and_prefix_includes_match
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('archives',), prefix='foo-')
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('archives',), check_arguments=flexmock(match_archives=None), prefix='foo-'
|
||||
)
|
||||
|
||||
assert flags == ('--match-archives', 'sh:foo-*')
|
||||
|
||||
@@ -284,11 +314,30 @@ def test_make_archive_filter_flags_with_data_check_and_prefix_includes_match_arc
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('data',), prefix='foo-')
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('data',), check_arguments=flexmock(match_archives=None), prefix='foo-'
|
||||
)
|
||||
|
||||
assert flags == ('--match-archives', 'sh:foo-*')
|
||||
|
||||
|
||||
def test_make_archive_filter_flags_prefers_check_arguments_match_archives_to_config_match_archives():
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
'baz-*', None, '1.2.3'
|
||||
).and_return(('--match-archives', 'sh:baz-*'))
|
||||
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3',
|
||||
{'match_archives': 'bar-{now}'}, # noqa: FS003
|
||||
('archives',),
|
||||
check_arguments=flexmock(match_archives='baz-*'),
|
||||
prefix='',
|
||||
)
|
||||
|
||||
assert flags == ('--match-archives', 'sh:baz-*')
|
||||
|
||||
|
||||
def test_make_archive_filter_flags_with_archives_check_and_empty_prefix_uses_archive_name_format_instead():
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
@@ -296,7 +345,11 @@ def test_make_archive_filter_flags_with_archives_check_and_empty_prefix_uses_arc
|
||||
).and_return(('--match-archives', 'sh:bar-*'))
|
||||
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {'archive_name_format': 'bar-{now}'}, ('archives',), prefix='' # noqa: FS003
|
||||
'1.2.3',
|
||||
{'archive_name_format': 'bar-{now}'}, # noqa: FS003
|
||||
('archives',),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
prefix='',
|
||||
)
|
||||
|
||||
assert flags == ('--match-archives', 'sh:bar-*')
|
||||
@@ -306,7 +359,9 @@ def test_make_archive_filter_flags_with_archives_check_and_none_prefix_omits_mat
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('archives',), prefix=None)
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('archives',), check_arguments=flexmock(match_archives=None), prefix=None
|
||||
)
|
||||
|
||||
assert flags == ()
|
||||
|
||||
@@ -315,7 +370,9 @@ def test_make_archive_filter_flags_with_repository_check_and_prefix_omits_match_
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('repository',), prefix='foo-')
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3', {}, ('repository',), check_arguments=flexmock(match_archives=None), prefix='foo-'
|
||||
)
|
||||
|
||||
assert flags == ()
|
||||
|
||||
@@ -324,7 +381,13 @@ def test_make_archive_filter_flags_with_default_checks_and_prefix_includes_match
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
flags = module.make_archive_filter_flags('1.2.3', {}, ('repository', 'archives'), prefix='foo-')
|
||||
flags = module.make_archive_filter_flags(
|
||||
'1.2.3',
|
||||
{},
|
||||
('repository', 'archives'),
|
||||
check_arguments=flexmock(match_archives=None),
|
||||
prefix='foo-',
|
||||
)
|
||||
|
||||
assert flags == ('--match-archives', 'sh:foo-*')
|
||||
|
||||
@@ -607,7 +670,7 @@ def test_upgrade_check_times_renames_stale_temporary_check_path():
|
||||
module.upgrade_check_times(flexmock(), flexmock())
|
||||
|
||||
|
||||
def test_check_archives_with_progress_calls_borg_with_progress_parameter():
|
||||
def test_check_archives_with_progress_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
config = {'check_last': None}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
@@ -634,12 +697,14 @@ def test_check_archives_with_progress_calls_borg_with_progress_parameter():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=True, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
progress=True,
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_repair_calls_borg_with_repair_parameter():
|
||||
def test_check_archives_with_repair_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
config = {'check_last': None}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
@@ -666,8 +731,10 @@ def test_check_archives_with_repair_calls_borg_with_repair_parameter():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=True, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
repair=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -701,6 +768,9 @@ def test_check_archives_calls_borg_with_parameters(checks):
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -723,6 +793,9 @@ def test_check_archives_with_json_error_raises():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -743,6 +816,9 @@ def test_check_archives_with_missing_json_keys_raises():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -769,11 +845,14 @@ def test_check_archives_with_extract_check_calls_extract_only():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
def test_check_archives_with_log_info_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
config = {'check_last': None}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
@@ -795,11 +874,14 @@ def test_check_archives_with_log_info_calls_borg_with_info_parameter():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
def test_check_archives_with_log_debug_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
config = {'check_last': None}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
@@ -821,6 +903,9 @@ def test_check_archives_with_log_debug_calls_borg_with_debug_parameter():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -841,6 +926,9 @@ def test_check_archives_without_any_checks_bails():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -867,12 +955,15 @@ def test_check_archives_with_local_path_calls_borg_via_local_path():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
local_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
def test_check_archives_with_remote_path_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
check_last = flexmock()
|
||||
config = {'check_last': check_last}
|
||||
@@ -894,12 +985,15 @@ def test_check_archives_with_remote_path_calls_borg_with_remote_path_parameters(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_log_json_calls_borg_with_log_json_parameters():
|
||||
def test_check_archives_with_log_json_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
check_last = flexmock()
|
||||
config = {'check_last': check_last}
|
||||
@@ -921,11 +1015,14 @@ def test_check_archives_with_log_json_calls_borg_with_log_json_parameters():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=True),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
def test_check_archives_with_lock_wait_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
check_last = flexmock()
|
||||
config = {'lock_wait': 5, 'check_last': check_last}
|
||||
@@ -947,6 +1044,9 @@ def test_check_archives_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -974,11 +1074,14 @@ def test_check_archives_with_retention_prefix():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
def test_check_archives_with_extra_borg_options_passes_through_to_borg():
|
||||
checks = ('repository',)
|
||||
config = {'check_last': None, 'extra_borg_options': {'check': '--extra --options'}}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
@@ -999,5 +1102,42 @@ def test_check_archives_with_extra_borg_options_calls_borg_with_extra_options():
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives=None
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_check_archives_with_match_archives_passes_through_to_borg():
|
||||
checks = ('archives',)
|
||||
config = {'check_last': None}
|
||||
flexmock(module.rinfo).should_receive('display_repository_info').and_return(
|
||||
'{"repository": {"id": "repo"}}'
|
||||
)
|
||||
flexmock(module).should_receive('upgrade_check_times')
|
||||
flexmock(module).should_receive('parse_checks')
|
||||
flexmock(module).should_receive('make_archive_filter_flags').and_return(
|
||||
('--match-archives', 'foo-*')
|
||||
)
|
||||
flexmock(module).should_receive('make_archives_check_id').and_return(None)
|
||||
flexmock(module).should_receive('filter_checks_on_frequency').and_return(checks)
|
||||
flexmock(module).should_receive('make_check_flags').and_return(('--match-archives', 'foo-*'))
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'check', '--match-archives', 'foo-*', 'repo'),
|
||||
extra_environment=None,
|
||||
).once()
|
||||
flexmock(module).should_receive('make_check_time_path')
|
||||
flexmock(module).should_receive('write_check_time')
|
||||
|
||||
module.check_archives(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='1.2.3',
|
||||
check_arguments=flexmock(
|
||||
progress=None, repair=None, only_checks=None, force=None, match_archives='foo-*'
|
||||
),
|
||||
global_arguments=flexmock(log_json=False),
|
||||
)
|
||||
|
||||
@@ -647,6 +647,53 @@ def test_create_archive_with_sources_and_used_config_paths_calls_borg_with_sourc
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_sources_and_used_config_paths_with_store_config_files_false_calls_borg_with_sources_and_no_config_paths():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
|
||||
flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('map_directories_to_devices').and_return({})
|
||||
flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
|
||||
flexmock(module).should_receive('expand_directories').with_args(('foo', 'bar')).and_return(
|
||||
('foo', 'bar')
|
||||
)
|
||||
flexmock(module).should_receive('expand_directories').with_args([]).and_return(())
|
||||
flexmock(module).should_receive('pattern_root_directories').and_return([])
|
||||
flexmock(module.os.path).should_receive('expanduser').and_raise(TypeError)
|
||||
flexmock(module).should_receive('expand_home_directories').and_return(())
|
||||
flexmock(module).should_receive('write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('make_list_filter_flags').and_return('FOO')
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module).should_receive('ensure_files_readable')
|
||||
flexmock(module).should_receive('make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('make_exclude_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_archive_flags').and_return(
|
||||
(f'repo::{DEFAULT_ARCHIVE_NAME}',)
|
||||
)
|
||||
environment = {'BORG_THINGY': 'YUP'}
|
||||
flexmock(module.environment).should_receive('make_environment').and_return(environment)
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
working_directory=None,
|
||||
extra_environment=environment,
|
||||
)
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository_path='repo',
|
||||
config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'store_config_files': False,
|
||||
},
|
||||
local_borg_version='1.2.3',
|
||||
global_arguments=flexmock(log_json=False, used_config_paths=['/etc/borgmatic/config.yaml']),
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_exclude_patterns_calls_borg_with_excludes():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
@@ -713,7 +760,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--info',),
|
||||
('borg', 'create', '--info') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
@@ -756,7 +803,7 @@ def test_create_archive_with_log_info_and_json_suppresses_most_borg_output():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
|
||||
working_directory=None,
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
@@ -798,7 +845,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--debug', '--show-rc'),
|
||||
('borg', 'create', '--debug', '--show-rc') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
@@ -841,7 +888,7 @@ def test_create_archive_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
|
||||
working_directory=None,
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
@@ -927,7 +974,7 @@ def test_create_archive_with_stats_and_dry_run_calls_borg_without_stats_paramete
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create', '--dry-run') + REPO_ARCHIVE_WITH_PATHS + ('--info',),
|
||||
('borg', 'create', '--dry-run', '--info') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
@@ -1802,7 +1849,7 @@ def test_create_archive_with_stats_calls_borg_with_stats_parameter_and_answer_ou
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--stats',),
|
||||
('borg', 'create', '--stats') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
output_file=None,
|
||||
borg_local_path='borg',
|
||||
@@ -1888,12 +1935,7 @@ def test_create_archive_with_progress_and_log_info_calls_borg_with_progress_para
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create')
|
||||
+ REPO_ARCHIVE_WITH_PATHS
|
||||
+ (
|
||||
'--info',
|
||||
'--progress',
|
||||
),
|
||||
('borg', 'create', '--info', '--progress') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=module.DO_NOT_CAPTURE,
|
||||
borg_local_path='borg',
|
||||
@@ -1937,7 +1979,7 @@ def test_create_archive_with_progress_calls_borg_with_progress_parameter():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--progress',),
|
||||
('borg', 'create', '--progress') + REPO_ARCHIVE_WITH_PATHS,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=module.DO_NOT_CAPTURE,
|
||||
borg_local_path='borg',
|
||||
@@ -1982,10 +2024,12 @@ def test_create_archive_with_progress_and_stream_processes_calls_borg_with_progr
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('collect_special_file_paths').and_return(())
|
||||
create_command = (
|
||||
('borg', 'create', '--one-file-system', '--read-special')
|
||||
+ REPO_ARCHIVE_WITH_PATHS
|
||||
+ ('--progress',)
|
||||
)
|
||||
'borg',
|
||||
'create',
|
||||
'--one-file-system',
|
||||
'--read-special',
|
||||
'--progress',
|
||||
) + REPO_ARCHIVE_WITH_PATHS
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
create_command + ('--dry-run', '--list'),
|
||||
processes=processes,
|
||||
@@ -2111,14 +2155,14 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('collect_special_file_paths').and_return(('special',))
|
||||
create_command = (
|
||||
create_flags = (
|
||||
'borg',
|
||||
'create',
|
||||
'--one-file-system',
|
||||
'--read-special',
|
||||
) + REPO_ARCHIVE_WITH_PATHS
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
create_command + ('--dry-run', '--list'),
|
||||
create_flags + ('--dry-run', '--list') + REPO_ARCHIVE_WITH_PATHS,
|
||||
processes=processes,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
@@ -2127,7 +2171,7 @@ def test_create_archive_with_stream_processes_adds_special_files_to_excludes():
|
||||
extra_environment=None,
|
||||
)
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
create_command + ('--exclude-from', '/excludes'),
|
||||
create_flags + ('--exclude-from', '/excludes') + REPO_ARCHIVE_WITH_PATHS,
|
||||
processes=processes,
|
||||
output_log_level=logging.INFO,
|
||||
output_file=None,
|
||||
@@ -2235,7 +2279,7 @@ def test_create_archive_with_json_calls_borg_with_json_parameter():
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
|
||||
working_directory=None,
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
@@ -2278,7 +2322,7 @@ def test_create_archive_with_stats_and_json_calls_borg_without_stats_parameter()
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'create') + REPO_ARCHIVE_WITH_PATHS + ('--json',),
|
||||
('borg', 'create', '--json') + REPO_ARCHIVE_WITH_PATHS,
|
||||
working_directory=None,
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
|
||||
@@ -19,28 +19,36 @@ def test_make_environment_with_ssh_command_should_set_environment():
|
||||
assert environment.get('BORG_RSH') == 'ssh -C'
|
||||
|
||||
|
||||
def test_make_environment_without_configuration_should_only_set_default_environment():
|
||||
def test_make_environment_without_configuration_should_not_set_environment():
|
||||
environment = module.make_environment({})
|
||||
|
||||
assert environment == {
|
||||
'BORG_RELOCATED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK': 'no',
|
||||
'BORG_CHECK_I_KNOW_WHAT_I_AM_DOING': 'NO',
|
||||
}
|
||||
assert environment == {}
|
||||
|
||||
|
||||
def test_make_environment_with_relocated_repo_access_should_override_default():
|
||||
def test_make_environment_with_relocated_repo_access_true_should_set_environment_yes():
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': True})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'yes'
|
||||
|
||||
|
||||
def test_make_environment_check_i_know_what_i_am_doing_should_override_default():
|
||||
def test_make_environment_with_relocated_repo_access_false_should_set_environment_no():
|
||||
environment = module.make_environment({'relocated_repo_access_is_ok': False})
|
||||
|
||||
assert environment.get('BORG_RELOCATED_REPO_ACCESS_IS_OK') == 'no'
|
||||
|
||||
|
||||
def test_make_environment_check_i_know_what_i_am_doing_true_should_set_environment_YES():
|
||||
environment = module.make_environment({'check_i_know_what_i_am_doing': True})
|
||||
|
||||
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'YES'
|
||||
|
||||
|
||||
def test_make_environment_check_i_know_what_i_am_doing_false_should_set_environment_NO():
|
||||
environment = module.make_environment({'check_i_know_what_i_am_doing': False})
|
||||
|
||||
assert environment.get('BORG_CHECK_I_KNOW_WHAT_I_AM_DOING') == 'NO'
|
||||
|
||||
|
||||
def test_make_environment_with_integer_variable_value():
|
||||
environment = module.make_environment({'borg_files_cache_ttl': 40})
|
||||
assert environment.get('BORG_FILES_CACHE_TTL') == '40'
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.logger
|
||||
from borgmatic.borg import export_key as module
|
||||
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def insert_execute_command_mock(command, output_file=module.DO_NOT_CAPTURE):
|
||||
borgmatic.logger.add_custom_log_levels()
|
||||
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
command,
|
||||
output_file=output_file,
|
||||
output_log_level=module.logging.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_required_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_remote_path_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--remote-path', 'borg1', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_umask_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--umask', '0770', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={'umask': '0770'},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_log_json_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--log-json', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=True),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_lock_wait_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--lock-wait', '5', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={'lock_wait': '5'},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--info', 'repo'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_log_debug_calls_borg_with_debug_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--debug', '--show-rc', 'repo'))
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_paper_flags():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=True, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_paper_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--paper', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=True, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_qr_html_flag():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', '--qr-html', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=True, path=None),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_calls_borg_with_path_argument():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo', 'dest'), output_file=None)
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='dest'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_already_existent_path_raises():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(FileExistsError):
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='dest'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_stdout_path_calls_borg_without_path_argument():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
insert_execute_command_mock(('borg', 'key', 'export', 'repo'))
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path='-'),
|
||||
global_arguments=flexmock(dry_run=False, log_json=False),
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_with_dry_run_skip_borg_call():
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
module.export_key(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
export_arguments=flexmock(paper=False, qr_html=False, path=None),
|
||||
global_arguments=flexmock(dry_run=True, log_json=False),
|
||||
)
|
||||
@@ -86,28 +86,28 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'match_archives, archive_name_format,feature_available,expected_result',
|
||||
'match_archives,archive_name_format,feature_available,expected_result',
|
||||
(
|
||||
(None, None, True, ()),
|
||||
(None, '', True, ()),
|
||||
(None, None, True, ('--match-archives', 'sh:{hostname}-*')), # noqa: FS003
|
||||
(None, '', True, ('--match-archives', 'sh:{hostname}-*')), # noqa: FS003
|
||||
(
|
||||
're:foo-.*',
|
||||
'{hostname}-{now}',
|
||||
'{hostname}-{now}', # noqa: FS003
|
||||
True,
|
||||
('--match-archives', 're:foo-.*'),
|
||||
), # noqa: FS003
|
||||
),
|
||||
(
|
||||
'sh:foo-*',
|
||||
'{hostname}-{now}',
|
||||
'{hostname}-{now}', # noqa: FS003
|
||||
False,
|
||||
('--glob-archives', 'foo-*'),
|
||||
), # noqa: FS003
|
||||
),
|
||||
(
|
||||
'foo-*',
|
||||
'{hostname}-{now}',
|
||||
'{hostname}-{now}', # noqa: FS003
|
||||
False,
|
||||
('--glob-archives', 'foo-*'),
|
||||
), # noqa: FS003
|
||||
),
|
||||
(
|
||||
None,
|
||||
'{hostname}-docs-{now}', # noqa: FS003
|
||||
@@ -133,7 +133,24 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
|
||||
False,
|
||||
('--glob-archives', '{hostname}-docs-*'), # noqa: FS003
|
||||
),
|
||||
(None, '{utcnow}-docs-{user}', False, ('--glob-archives', '*-docs-{user}')), # noqa: FS003
|
||||
(
|
||||
None,
|
||||
'{now}', # noqa: FS003
|
||||
False,
|
||||
(),
|
||||
),
|
||||
(
|
||||
None,
|
||||
'{now}', # noqa: FS003
|
||||
True,
|
||||
(),
|
||||
),
|
||||
(
|
||||
None,
|
||||
'{utcnow}-docs-{user}', # noqa: FS003
|
||||
False,
|
||||
('--glob-archives', '*-docs-{user}'), # noqa: FS003
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_make_match_archives_flags_makes_flags_with_globs(
|
||||
@@ -147,3 +164,53 @@ def test_make_match_archives_flags_makes_flags_with_globs(
|
||||
)
|
||||
== expected_result
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_without_archive_flags_bails():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--do-stuff'), '{}')
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_zero_archives_warns():
|
||||
flexmock(module.logger).should_receive('warning').twice()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'), '{"archives": []}'
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_match_archives_and_zero_archives_warns():
|
||||
flexmock(module.logger).should_receive('warning').twice()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'), '{"archives": []}'
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_one_archive_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--glob-archives', 'foo*'), '{"archives": [{"name": "foo"]}'
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_match_archives_and_one_archive_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(
|
||||
('borg', '--match-archives', 'foo*'), '{"archives": [{"name": "foo"]}'
|
||||
)
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_invalid_json_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{"archives": [}')
|
||||
|
||||
|
||||
def test_warn_for_aggressive_archive_flags_with_glob_archives_and_json_missing_archives_does_not_warn():
|
||||
flexmock(module.logger).should_receive('warning').never()
|
||||
|
||||
module.warn_for_aggressive_archive_flags(('borg', '--glob-archives', 'foo*'), '{}')
|
||||
|
||||
+157
-210
@@ -8,224 +8,178 @@ from borgmatic.borg import info as module
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
def test_display_archives_info_calls_borg_with_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
def test_make_info_command_constructs_borg_info_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_log_info_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--info', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.INFO)
|
||||
module.display_archives_info(
|
||||
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--info', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_log_info_and_json_suppresses_most_borg_output():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_log_info_and_json_omits_borg_logging_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'info', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.INFO)
|
||||
json_output = module.display_archives_info(
|
||||
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=True, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
assert command == ('borg', 'info', '--json', '--repo', 'repo')
|
||||
|
||||
|
||||
def test_display_archives_info_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
def test_make_info_command_with_log_debug_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--debug', '--show-rc', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--debug', '--show-rc', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_log_debug_and_json_suppresses_most_borg_output():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_log_debug_and_json_omits_borg_logging_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'info', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
json_output = module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=True, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
assert command == ('borg', 'info', '--json', '--repo', 'repo')
|
||||
|
||||
|
||||
def test_display_archives_info_with_json_calls_borg_with_json_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
def test_make_info_command_with_json_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(('--json',))
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'info', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
|
||||
json_output = module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=True, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert json_output == '[]'
|
||||
assert command == ('borg', 'info', '--json', '--repo', 'repo')
|
||||
|
||||
|
||||
def test_display_archives_info_with_archive_calls_borg_with_match_archives_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
def test_make_info_command_with_archive_uses_match_archives_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
'archive', None, '2.3.4'
|
||||
).and_return(('--match-archives', 'archive'))
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'archive', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive='archive', json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'archive', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_local_path_calls_borg_via_local_path():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_local_path_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'info', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg1',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg1',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
command == ('borg1', 'info', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_remote_path_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||
'remote-path', 'borg1'
|
||||
@@ -235,27 +189,21 @@ def test_display_archives_info_with_remote_path_calls_borg_with_remote_path_para
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path='borg1',
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--remote-path', 'borg1', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_log_json_calls_borg_with_log_json_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_log_json_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('log-json', True).and_return(
|
||||
('--log-json',)
|
||||
@@ -265,26 +213,21 @@ def test_display_archives_info_with_log_json_calls_borg_with_log_json_parameters
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--log-json', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=True),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--log-json', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_lock_wait_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args('lock-wait', 5).and_return(
|
||||
('--lock-wait', '5')
|
||||
@@ -295,26 +238,21 @@ def test_display_archives_info_with_lock_wait_calls_borg_with_lock_wait_paramete
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
config = {'lock_wait': 5}
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--lock-wait', '5', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config=config,
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--lock-wait', '5', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_transforms_prefix_into_match_archives_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_transforms_prefix_into_match_archives_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||
'match-archives', 'sh:foo*'
|
||||
@@ -324,26 +262,21 @@ def test_display_archives_info_transforms_prefix_into_match_archives_parameters(
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix='foo'),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_prefers_prefix_over_archive_name_format():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_prefers_prefix_over_archive_name_format():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags').with_args(
|
||||
'match-archives', 'sh:foo*'
|
||||
@@ -353,52 +286,42 @@ def test_display_archives_info_prefers_prefix_over_archive_name_format():
|
||||
).and_return(())
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={'archive_name_format': 'bar-{now}'}, # noqa: FS003
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix='foo'),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'sh:foo*', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_transforms_archive_name_format_into_match_archives_parameters():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_transforms_archive_name_format_into_match_archives_flags():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, 'bar-{now}', '2.3.4' # noqa: FS003
|
||||
).and_return(('--match-archives', 'sh:bar-*'))
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'sh:bar-*', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={'archive_name_format': 'bar-{now}'}, # noqa: FS003
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'sh:bar-*', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_with_match_archives_option_calls_borg_with_match_archives_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_match_archives_option_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
'sh:foo-*', 'bar-{now}', '2.3.4' # noqa: FS003
|
||||
@@ -406,14 +329,8 @@ def test_display_archives_with_match_archives_option_calls_borg_with_match_archi
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'sh:foo-*', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={
|
||||
'archive_name_format': 'bar-{now}', # noqa: FS003
|
||||
@@ -422,12 +339,14 @@ def test_display_archives_with_match_archives_option_calls_borg_with_match_archi
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'sh:foo-*', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_with_match_archives_flag_calls_borg_with_match_archives_parameter():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_match_archives_flag_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
'sh:foo-*', 'bar-{now}', '2.3.4' # noqa: FS003
|
||||
@@ -435,26 +354,22 @@ def test_display_archives_with_match_archives_flag_calls_borg_with_match_archive
|
||||
flexmock(module.flags).should_receive('make_flags_from_arguments').and_return(())
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', '--match-archives', 'sh:foo-*', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={'archive_name_format': 'bar-{now}'}, # noqa: FS003
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives='sh:foo-*'),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', '--match-archives', 'sh:foo-*', '--repo', 'repo')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('argument_name', ('sort_by', 'first', 'last'))
|
||||
def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
def test_make_info_command_passes_arguments_through_to_command(argument_name):
|
||||
flag_name = f"--{argument_name.replace('_', ' ')}"
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
@@ -465,14 +380,8 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', flag_name, 'value', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
|
||||
module.display_archives_info(
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
@@ -480,12 +389,14 @@ def test_display_archives_info_passes_through_arguments_to_borg(argument_name):
|
||||
info_arguments=flexmock(
|
||||
archive=None, json=False, prefix=None, match_archives=None, **{argument_name: 'value'}
|
||||
),
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == ('borg', 'info', flag_name, 'value', '--repo', 'repo')
|
||||
|
||||
def test_display_archives_info_with_date_based_matching_calls_borg_with_date_based_flags():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
|
||||
def test_make_info_command_with_date_based_matching_passes_through_to_command():
|
||||
flexmock(module.flags).should_receive('make_flags').and_return(())
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').with_args(
|
||||
None, None, '2.3.4'
|
||||
@@ -494,26 +405,6 @@ def test_display_archives_info_with_date_based_matching_calls_borg_with_date_bas
|
||||
('--newer', '1d', '--newest', '1y', '--older', '1m', '--oldest', '1w')
|
||||
)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('--repo', 'repo'))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'borg',
|
||||
'info',
|
||||
'--newer',
|
||||
'1d',
|
||||
'--newest',
|
||||
'1y',
|
||||
'--older',
|
||||
'1m',
|
||||
'--oldest',
|
||||
'1w',
|
||||
'--repo',
|
||||
'repo',
|
||||
),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
)
|
||||
info_arguments = flexmock(
|
||||
archive=None,
|
||||
json=False,
|
||||
@@ -524,10 +415,66 @@ def test_display_archives_info_with_date_based_matching_calls_borg_with_date_bas
|
||||
older='1m',
|
||||
oldest='1w',
|
||||
)
|
||||
module.display_archives_info(
|
||||
|
||||
command = module.make_info_command(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=info_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
)
|
||||
|
||||
assert command == (
|
||||
'borg',
|
||||
'info',
|
||||
'--newer',
|
||||
'1d',
|
||||
'--newest',
|
||||
'1y',
|
||||
'--older',
|
||||
'1m',
|
||||
'--oldest',
|
||||
'1w',
|
||||
'--repo',
|
||||
'repo',
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_calls_two_commands():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module).should_receive('make_info_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=False, prefix=None, match_archives=None),
|
||||
)
|
||||
|
||||
|
||||
def test_display_archives_info_with_json_calls_json_command_only():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module).should_receive('make_info_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
json_output = flexmock()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.display_archives_info(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='2.3.4',
|
||||
global_arguments=flexmock(log_json=False),
|
||||
info_arguments=flexmock(archive=None, json=True, prefix=None, match_archives=None),
|
||||
)
|
||||
== json_output
|
||||
)
|
||||
|
||||
@@ -118,6 +118,19 @@ def test_make_prune_flags_without_prefix_uses_archive_name_format_instead():
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_make_prune_flags_ignores_keep_exclude_tags_in_config():
|
||||
config = {
|
||||
'keep_daily': 1,
|
||||
'keep_exclude_tags': True,
|
||||
}
|
||||
flexmock(module.feature).should_receive('available').and_return(True)
|
||||
flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
|
||||
|
||||
result = module.make_prune_flags(config, local_borg_version='1.2.3')
|
||||
|
||||
assert result == ('--keep-daily', '1')
|
||||
|
||||
|
||||
PRUNE_COMMAND = ('borg', 'prune', '--keep-daily', '1', '--keep-weekly', '2', '--keep-monthly', '3')
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ def test_display_repository_info_calls_borg_with_flags():
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--json', '--repo', 'repo'),
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -40,6 +46,12 @@ def test_display_repository_info_without_borg_features_calls_borg_with_info_sub_
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--json', 'repo'),
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'info', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -67,6 +79,12 @@ def test_display_repository_info_with_log_info_calls_borg_with_info_flag():
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--info', '--json', '--repo', 'repo'),
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--info', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -99,6 +117,7 @@ def test_display_repository_info_with_log_info_and_json_suppresses_most_borg_out
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
|
||||
insert_logging_mock(logging.INFO)
|
||||
json_output = module.display_repository_info(
|
||||
@@ -123,6 +142,12 @@ def test_display_repository_info_with_log_debug_calls_borg_with_debug_flag():
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--debug', '--show-rc', '--json', '--repo', 'repo'),
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--debug', '--show-rc', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -156,6 +181,7 @@ def test_display_repository_info_with_log_debug_and_json_suppresses_most_borg_ou
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
json_output = module.display_repository_info(
|
||||
@@ -185,6 +211,7 @@ def test_display_repository_info_with_json_calls_borg_with_json_flag():
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
|
||||
json_output = module.display_repository_info(
|
||||
repository_path='repo',
|
||||
@@ -208,6 +235,12 @@ def test_display_repository_info_with_local_path_calls_borg_via_local_path():
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg1', 'rinfo', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg1', 'rinfo', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -236,6 +269,12 @@ def test_display_repository_info_with_remote_path_calls_borg_with_remote_path_fl
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--remote-path', 'borg1', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--remote-path', 'borg1', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -264,6 +303,12 @@ def test_display_repository_info_with_log_json_calls_borg_with_log_json_flags():
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--log-json', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--log-json', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
@@ -292,6 +337,12 @@ def test_display_repository_info_with_lock_wait_calls_borg_with_lock_wait_flags(
|
||||
)
|
||||
)
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('borg', 'rinfo', '--lock-wait', '5', '--json', '--repo', 'repo'),
|
||||
extra_environment=None,
|
||||
borg_local_path='borg',
|
||||
).and_return('[]')
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rinfo', '--lock-wait', '5', '--repo', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
|
||||
@@ -559,66 +559,39 @@ def test_make_rlist_command_with_match_archives_calls_borg_with_match_archives_f
|
||||
assert command == ('borg', 'list', '--match-archives', 'foo-*', 'repo')
|
||||
|
||||
|
||||
def test_list_repository_calls_borg_with_flags():
|
||||
def test_list_repository_calls_two_commands():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
rlist_arguments = argparse.Namespace(json=False)
|
||||
global_arguments = flexmock()
|
||||
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module).should_receive('make_rlist_command').with_args(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
rlist_arguments=rlist_arguments,
|
||||
global_arguments=global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
).and_return(('borg', 'rlist', 'repo'))
|
||||
flexmock(module).should_receive('make_rlist_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
('borg', 'rlist', 'repo'),
|
||||
output_log_level=module.borgmatic.logger.ANSWER,
|
||||
borg_local_path='borg',
|
||||
extra_environment=None,
|
||||
).once()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').once()
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags')
|
||||
flexmock(module).should_receive('execute_command').once()
|
||||
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
rlist_arguments=rlist_arguments,
|
||||
global_arguments=global_arguments,
|
||||
rlist_arguments=argparse.Namespace(json=False),
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
|
||||
|
||||
def test_list_repository_with_json_returns_borg_output():
|
||||
def test_list_repository_with_json_calls_json_command_only():
|
||||
flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
|
||||
rlist_arguments = argparse.Namespace(json=True)
|
||||
global_arguments = flexmock()
|
||||
json_output = flexmock()
|
||||
|
||||
flexmock(module.feature).should_receive('available').and_return(False)
|
||||
flexmock(module).should_receive('make_rlist_command').with_args(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
rlist_arguments=rlist_arguments,
|
||||
global_arguments=global_arguments,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
).and_return(('borg', 'rlist', 'repo'))
|
||||
flexmock(module).should_receive('make_rlist_command')
|
||||
flexmock(module.environment).should_receive('make_environment')
|
||||
json_output = flexmock()
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').and_return(json_output)
|
||||
flexmock(module.flags).should_receive('warn_for_aggressive_archive_flags').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.list_repository(
|
||||
repository_path='repo',
|
||||
config={},
|
||||
local_borg_version='1.2.3',
|
||||
rlist_arguments=rlist_arguments,
|
||||
global_arguments=global_arguments,
|
||||
rlist_arguments=argparse.Namespace(json=True),
|
||||
global_arguments=flexmock(),
|
||||
)
|
||||
== json_output
|
||||
)
|
||||
|
||||
@@ -23,6 +23,16 @@ def test_run_configuration_runs_actions_for_each_repository():
|
||||
assert results == expected_results
|
||||
|
||||
|
||||
def test_run_configuration_with_skip_actions_does_not_raise():
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_return(flexmock())
|
||||
flexmock(module).should_receive('run_actions').and_return(flexmock()).and_return(flexmock())
|
||||
config = {'repositories': [{'path': 'foo'}, {'path': 'bar'}], 'skip_actions': ['compact']}
|
||||
arguments = {'global': flexmock(monitoring_verbosity=1)}
|
||||
|
||||
list(module.run_configuration('test.yaml', config, arguments))
|
||||
|
||||
|
||||
def test_run_configuration_with_invalid_borg_version_errors():
|
||||
flexmock(module).should_receive('verbosity_to_log_level').and_return(logging.INFO)
|
||||
flexmock(module.borg_version).should_receive('local_borg_version').and_raise(ValueError)
|
||||
@@ -504,6 +514,24 @@ def test_run_actions_runs_create():
|
||||
assert result == (expected,)
|
||||
|
||||
|
||||
def test_run_actions_with_skip_actions_skips_create():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(borgmatic.actions.create).should_receive('run_create').never()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'create': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': [], 'skip_actions': ['create']},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_prune():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
@@ -522,6 +550,24 @@ def test_run_actions_runs_prune():
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_with_skip_actions_skips_prune():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(borgmatic.actions.prune).should_receive('run_prune').never()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'prune': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': [], 'skip_actions': ['prune']},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_compact():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
@@ -540,6 +586,24 @@ def test_run_actions_runs_compact():
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_with_skip_actions_skips_compact():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(borgmatic.actions.compact).should_receive('run_compact').never()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'compact': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': [], 'skip_actions': ['compact']},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_check_when_repository_enabled_for_checks():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
@@ -578,6 +642,25 @@ def test_run_actions_skips_check_when_repository_not_enabled_for_checks():
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_with_skip_actions_skips_check():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(module.checks).should_receive('repository_enabled_for_checks').and_return(True)
|
||||
flexmock(borgmatic.actions.check).should_receive('run_check').never()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'check': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': [], 'skip_actions': ['check']},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_extract():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
@@ -748,6 +831,24 @@ def test_run_actions_runs_break_lock():
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_export_key():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
flexmock(borgmatic.actions.export_key).should_receive('run_export_key').once()
|
||||
|
||||
tuple(
|
||||
module.run_actions(
|
||||
arguments={'global': flexmock(dry_run=False, log_file='foo'), 'export': flexmock()},
|
||||
config_filename=flexmock(),
|
||||
config={'repositories': []},
|
||||
local_path=flexmock(),
|
||||
remote_path=flexmock(),
|
||||
local_borg_version=flexmock(),
|
||||
repository={'path': 'repo'},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_run_actions_runs_borg():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.command).should_receive('execute_hook')
|
||||
@@ -831,7 +932,7 @@ def test_log_record_with_suppress_does_not_raise():
|
||||
|
||||
|
||||
def test_log_error_records_generates_output_logs_for_message_only():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).once()
|
||||
|
||||
logs = tuple(module.log_error_records('Error'))
|
||||
|
||||
@@ -839,7 +940,7 @@ def test_log_error_records_generates_output_logs_for_message_only():
|
||||
|
||||
|
||||
def test_log_error_records_generates_output_logs_for_called_process_error_with_bytes_ouput():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).times(3)
|
||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||
|
||||
logs = tuple(
|
||||
@@ -851,7 +952,7 @@ def test_log_error_records_generates_output_logs_for_called_process_error_with_b
|
||||
|
||||
|
||||
def test_log_error_records_generates_output_logs_for_called_process_error_with_string_ouput():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).times(3)
|
||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||
|
||||
logs = tuple(
|
||||
@@ -862,8 +963,22 @@ def test_log_error_records_generates_output_logs_for_called_process_error_with_s
|
||||
assert any(log for log in logs if 'error output' in str(log))
|
||||
|
||||
|
||||
def test_log_error_records_splits_called_process_error_with_multiline_ouput_into_multiple_logs():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).times(4)
|
||||
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||
|
||||
logs = tuple(
|
||||
module.log_error_records(
|
||||
'Error', subprocess.CalledProcessError(1, 'ls', 'error output\nanother line')
|
||||
)
|
||||
)
|
||||
|
||||
assert {log['levelno'] for log in logs} == {logging.CRITICAL}
|
||||
assert any(log for log in logs if 'error output' in str(log))
|
||||
|
||||
|
||||
def test_log_error_records_generates_logs_for_value_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).twice()
|
||||
|
||||
logs = tuple(module.log_error_records('Error', ValueError()))
|
||||
|
||||
@@ -871,7 +986,7 @@ def test_log_error_records_generates_logs_for_value_error():
|
||||
|
||||
|
||||
def test_log_error_records_generates_logs_for_os_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').replace_with(dict).twice()
|
||||
|
||||
logs = tuple(module.log_error_records('Error', OSError()))
|
||||
|
||||
@@ -879,7 +994,7 @@ def test_log_error_records_generates_logs_for_os_error():
|
||||
|
||||
|
||||
def test_log_error_records_generates_nothing_for_other_error():
|
||||
flexmock(module).should_receive('log_record').replace_with(dict)
|
||||
flexmock(module).should_receive('log_record').never()
|
||||
|
||||
logs = tuple(module.log_error_records('Error', KeyError()))
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@ from borgmatic.config import checks as module
|
||||
|
||||
|
||||
def test_repository_enabled_for_checks_defaults_to_enabled_for_all_repositories():
|
||||
enabled = module.repository_enabled_for_checks('repo.borg', consistency={})
|
||||
enabled = module.repository_enabled_for_checks('repo.borg', config={})
|
||||
|
||||
assert enabled
|
||||
|
||||
|
||||
def test_repository_enabled_for_checks_is_enabled_for_specified_repositories():
|
||||
enabled = module.repository_enabled_for_checks(
|
||||
'repo.borg', consistency={'check_repositories': ['repo.borg', 'other.borg']}
|
||||
'repo.borg', config={'check_repositories': ['repo.borg', 'other.borg']}
|
||||
)
|
||||
|
||||
assert enabled
|
||||
@@ -17,7 +17,7 @@ def test_repository_enabled_for_checks_is_enabled_for_specified_repositories():
|
||||
|
||||
def test_repository_enabled_for_checks_is_disabled_for_other_repositories():
|
||||
enabled = module.repository_enabled_for_checks(
|
||||
'repo.borg', consistency={'check_repositories': ['other.borg']}
|
||||
'repo.borg', config={'check_repositories': ['other.borg']}
|
||||
)
|
||||
|
||||
assert not enabled
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import constants as module
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,expected_value',
|
||||
(
|
||||
('3', 3),
|
||||
('0', 0),
|
||||
('-3', -3),
|
||||
('1234', 1234),
|
||||
('true', True),
|
||||
('True', True),
|
||||
('false', False),
|
||||
('False', False),
|
||||
('thing', 'thing'),
|
||||
({}, {}),
|
||||
({'foo': 'bar'}, {'foo': 'bar'}),
|
||||
([], []),
|
||||
(['foo', 'bar'], ['foo', 'bar']),
|
||||
),
|
||||
)
|
||||
def test_coerce_scalar_converts_value(value, expected_value):
|
||||
assert module.coerce_scalar(value) == expected_value
|
||||
|
||||
|
||||
def test_apply_constants_with_empty_constants_passes_through_value():
|
||||
assert module.apply_constants(value='thing', constants={}) == 'thing'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'value,expected_value',
|
||||
(
|
||||
(None, None),
|
||||
('thing', 'thing'),
|
||||
('{foo}', 'bar'),
|
||||
('abc{foo}', 'abcbar'),
|
||||
('{foo}xyz', 'barxyz'),
|
||||
('{foo}{baz}', 'barquux'),
|
||||
('{int}', '3'),
|
||||
('{bool}', 'True'),
|
||||
(['thing', 'other'], ['thing', 'other']),
|
||||
(['thing', '{foo}'], ['thing', 'bar']),
|
||||
(['{foo}', '{baz}'], ['bar', 'quux']),
|
||||
({'key': 'value'}, {'key': 'value'}),
|
||||
({'key': '{foo}'}, {'key': 'bar'}),
|
||||
(3, 3),
|
||||
(True, True),
|
||||
(False, False),
|
||||
),
|
||||
)
|
||||
def test_apply_constants_makes_string_substitutions(value, expected_value):
|
||||
flexmock(module).should_receive('coerce_scalar').replace_with(lambda value: value)
|
||||
constants = {'foo': 'bar', 'baz': 'quux', 'int': 3, 'bool': True}
|
||||
|
||||
assert module.apply_constants(value, constants) == expected_value
|
||||
@@ -7,7 +7,7 @@ from borgmatic.config import generate as module
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedMap').replace_with(OrderedDict)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_object')
|
||||
schema = {
|
||||
'type': 'object',
|
||||
@@ -32,7 +32,7 @@ def test_schema_to_sample_configuration_generates_config_map_with_examples():
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_strings_with_example():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
schema = {'type': 'array', 'items': {'type': 'string'}, 'example': ['hi']}
|
||||
|
||||
@@ -42,7 +42,7 @@ def test_schema_to_sample_configuration_generates_config_sequence_of_strings_wit
|
||||
|
||||
|
||||
def test_schema_to_sample_configuration_generates_config_sequence_of_maps_with_examples():
|
||||
flexmock(module.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module.ruamel.yaml.comments).should_receive('CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('add_comments_to_configuration_sequence')
|
||||
flexmock(module).should_receive('add_comments_to_configuration_object')
|
||||
schema = {
|
||||
@@ -71,7 +71,7 @@ def test_merge_source_configuration_into_destination_inserts_map_fields():
|
||||
destination_config = {'foo': 'dest1', 'bar': 'dest2'}
|
||||
source_config = {'foo': 'source1', 'baz': 'source2'}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_merge_source_configuration_into_destination_inserts_nested_map_fields()
|
||||
destination_config = {'foo': {'first': 'dest1', 'second': 'dest2'}, 'bar': 'dest3'}
|
||||
source_config = {'foo': {'first': 'source1'}}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
||||
@@ -93,7 +93,7 @@ def test_merge_source_configuration_into_destination_inserts_sequence_fields():
|
||||
destination_config = {'foo': ['dest1', 'dest2'], 'bar': ['dest3'], 'baz': ['dest4']}
|
||||
source_config = {'foo': ['source1'], 'bar': ['source2', 'source3']}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
||||
@@ -108,7 +108,7 @@ def test_merge_source_configuration_into_destination_inserts_sequence_of_maps():
|
||||
destination_config = {'foo': [{'first': 'dest1', 'second': 'dest2'}], 'bar': 'dest3'}
|
||||
source_config = {'foo': [{'first': 'source1'}, {'other': 'source2'}]}
|
||||
flexmock(module).should_receive('remove_commented_out_sentinel')
|
||||
flexmock(module).should_receive('yaml.comments.CommentedSeq').replace_with(list)
|
||||
flexmock(module).should_receive('ruamel.yaml.comments.CommentedSeq').replace_with(list)
|
||||
|
||||
module.merge_source_configuration_into_destination(destination_config, source_config)
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.config import load as module
|
||||
|
||||
|
||||
def test_probe_and_include_file_with_absolute_path_skips_probing():
|
||||
config = flexmock()
|
||||
flexmock(module).should_receive('load_configuration').with_args('/etc/include.yaml').and_return(
|
||||
config
|
||||
).once()
|
||||
|
||||
assert module.probe_and_include_file('/etc/include.yaml', ['/etc', '/var']) == config
|
||||
|
||||
|
||||
def test_probe_and_include_file_with_relative_path_probes_include_directories():
|
||||
config = flexmock()
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/etc/include.yaml').and_return(
|
||||
False
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/var/include.yaml').and_return(
|
||||
True
|
||||
)
|
||||
flexmock(module).should_receive('load_configuration').with_args('/etc/include.yaml').never()
|
||||
flexmock(module).should_receive('load_configuration').with_args('/var/include.yaml').and_return(
|
||||
config
|
||||
).once()
|
||||
|
||||
assert module.probe_and_include_file('include.yaml', ['/etc', '/var']) == config
|
||||
|
||||
|
||||
def test_probe_and_include_file_with_relative_path_and_missing_files_raises():
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module).should_receive('load_configuration').never()
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
module.probe_and_include_file('include.yaml', ['/etc', '/var'])
|
||||
@@ -77,6 +77,11 @@ from borgmatic.config import normalize as module
|
||||
{'bar': 'baz', 'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{'location': {}, 'consistency': {'prefix': 'foo'}},
|
||||
{'prefix': 'foo'},
|
||||
True,
|
||||
),
|
||||
(
|
||||
{},
|
||||
{},
|
||||
@@ -111,6 +116,13 @@ def test_normalize_sections_with_different_umask_values_raises():
|
||||
module.normalize_sections('test.yaml', config)
|
||||
|
||||
|
||||
def test_normalize_sections_with_only_scalar_raises():
|
||||
config = 33
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.normalize_sections('test.yaml', config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'config,expected_config,produces_logs',
|
||||
(
|
||||
|
||||
@@ -44,6 +44,24 @@ def test_set_values_with_multiple_keys_updates_hierarchy():
|
||||
assert config == {'option': {'key': 'value', 'other': 'other_value'}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'schema,option_keys,expected_type',
|
||||
(
|
||||
({'properties': {'foo': {'type': 'array'}}}, ('foo',), 'array'),
|
||||
(
|
||||
{'properties': {'foo': {'properties': {'bar': {'type': 'array'}}}}},
|
||||
('foo', 'bar'),
|
||||
'array',
|
||||
),
|
||||
({'properties': {'foo': {'type': 'array'}}}, ('other',), None),
|
||||
({'properties': {'foo': {'description': 'stuff'}}}, ('foo',), None),
|
||||
({}, ('foo',), None),
|
||||
),
|
||||
)
|
||||
def test_type_for_option_grabs_type_if_found_in_schema(schema, option_keys, expected_type):
|
||||
assert module.type_for_option(schema, option_keys) == expected_type
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'key,expected_key',
|
||||
(
|
||||
@@ -63,51 +81,64 @@ def test_strip_section_names_passes_through_key_without_section_name(key, expect
|
||||
|
||||
def test_parse_overrides_splits_keys_and_values():
|
||||
flexmock(module).should_receive('strip_section_names').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('type_for_option').and_return('string')
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(
|
||||
lambda value, option_type: value
|
||||
)
|
||||
raw_overrides = ['option.my_option=value1', 'other_option=value2']
|
||||
expected_result = (
|
||||
(('option', 'my_option'), 'value1'),
|
||||
(('other_option'), 'value2'),
|
||||
)
|
||||
|
||||
module.parse_overrides(raw_overrides) == expected_result
|
||||
module.parse_overrides(raw_overrides, schema={}) == expected_result
|
||||
|
||||
|
||||
def test_parse_overrides_allows_value_with_equal_sign():
|
||||
flexmock(module).should_receive('strip_section_names').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('type_for_option').and_return('string')
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(
|
||||
lambda value, option_type: value
|
||||
)
|
||||
raw_overrides = ['option=this===value']
|
||||
expected_result = ((('option',), 'this===value'),)
|
||||
|
||||
module.parse_overrides(raw_overrides) == expected_result
|
||||
module.parse_overrides(raw_overrides, schema={}) == expected_result
|
||||
|
||||
|
||||
def test_parse_overrides_raises_on_missing_equal_sign():
|
||||
flexmock(module).should_receive('strip_section_names').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('type_for_option').and_return('string')
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(
|
||||
lambda value, option_type: value
|
||||
)
|
||||
raw_overrides = ['option']
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_overrides(raw_overrides)
|
||||
module.parse_overrides(raw_overrides, schema={})
|
||||
|
||||
|
||||
def test_parse_overrides_raises_on_invalid_override_value():
|
||||
flexmock(module).should_receive('strip_section_names').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('type_for_option').and_return('string')
|
||||
flexmock(module).should_receive('convert_value_type').and_raise(ruamel.yaml.parser.ParserError)
|
||||
raw_overrides = ['option=[in valid]']
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_overrides(raw_overrides)
|
||||
module.parse_overrides(raw_overrides, schema={})
|
||||
|
||||
|
||||
def test_parse_overrides_allows_value_with_single_key():
|
||||
flexmock(module).should_receive('strip_section_names').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(lambda value: value)
|
||||
flexmock(module).should_receive('type_for_option').and_return('string')
|
||||
flexmock(module).should_receive('convert_value_type').replace_with(
|
||||
lambda value, option_type: value
|
||||
)
|
||||
raw_overrides = ['option=value']
|
||||
expected_result = ((('option',), 'value'),)
|
||||
|
||||
module.parse_overrides(raw_overrides) == expected_result
|
||||
module.parse_overrides(raw_overrides, schema={}) == expected_result
|
||||
|
||||
|
||||
def test_parse_overrides_handles_empty_overrides():
|
||||
module.parse_overrides(raw_overrides=None) == ()
|
||||
module.parse_overrides(raw_overrides=None, schema={}) == ()
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import apprise
|
||||
from apprise import NotifyFormat, NotifyType
|
||||
from flexmock import flexmock
|
||||
|
||||
import borgmatic.hooks.monitor
|
||||
from borgmatic.hooks import apprise as module
|
||||
|
||||
TOPIC = 'borgmatic-unit-testing'
|
||||
|
||||
|
||||
def mock_apprise():
|
||||
apprise_mock = flexmock(
|
||||
add=lambda servers: None, notify=lambda title, body, body_format, notify_type: None
|
||||
)
|
||||
flexmock(apprise.Apprise).new_instances(apprise_mock)
|
||||
return apprise_mock
|
||||
|
||||
|
||||
def test_ping_monitor_adheres_dry_run():
|
||||
mock_apprise().should_receive('notify').never()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_does_not_hit_with_no_states():
|
||||
mock_apprise().should_receive('notify').never()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': []},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_fail_by_default():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='A borgmatic FAIL event happened',
|
||||
body='A borgmatic FAIL event happened',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.FAILURE,
|
||||
).once()
|
||||
|
||||
for state in borgmatic.hooks.monitor.State:
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
|
||||
{},
|
||||
'config.yaml',
|
||||
state,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_with_finish_default_config():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='A borgmatic FINISH event happened',
|
||||
body='A borgmatic FINISH event happened',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.SUCCESS,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['finish']},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FINISH,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_with_start_default_config():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='A borgmatic START event happened',
|
||||
body='A borgmatic START event happened',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.INFO,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['start']},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.START,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_with_fail_default_config():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='A borgmatic FAIL event happened',
|
||||
body='A borgmatic FAIL event happened',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.FAILURE,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['fail']},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_hits_with_log_default_config():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='A borgmatic LOG event happened',
|
||||
body='A borgmatic LOG event happened',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.INFO,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}], 'states': ['log']},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.LOG,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_passes_through_custom_message_title():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='foo',
|
||||
body='bar',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.FAILURE,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{
|
||||
'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}],
|
||||
'states': ['fail'],
|
||||
'fail': {'title': 'foo', 'body': 'bar'},
|
||||
},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_passes_through_custom_message_body():
|
||||
mock_apprise().should_receive('notify').with_args(
|
||||
title='',
|
||||
body='baz',
|
||||
body_format=NotifyFormat.TEXT,
|
||||
notify_type=NotifyType.FAILURE,
|
||||
).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{
|
||||
'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}],
|
||||
'states': ['fail'],
|
||||
'fail': {'body': 'baz'},
|
||||
},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_pings_multiple_services():
|
||||
mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
|
||||
|
||||
module.ping_monitor(
|
||||
{
|
||||
'services': [
|
||||
{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'},
|
||||
{'url': f'ntfy://{TOPIC}', 'label': 'ntfy'},
|
||||
]
|
||||
},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_logs_info_for_no_services():
|
||||
flexmock(module.logger).should_receive('info').once()
|
||||
|
||||
module.ping_monitor(
|
||||
{'services': []},
|
||||
{},
|
||||
'config.yaml',
|
||||
borgmatic.hooks.monitor.State.FAIL,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
|
||||
|
||||
def test_ping_monitor_logs_warning_when_notify_fails():
|
||||
mock_apprise().should_receive('notify').and_return(False)
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
|
||||
for state in borgmatic.hooks.monitor.State:
|
||||
module.ping_monitor(
|
||||
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
|
||||
{},
|
||||
'config.yaml',
|
||||
state,
|
||||
monitoring_log_level=1,
|
||||
dry_run=False,
|
||||
)
|
||||
@@ -4,34 +4,36 @@ from flexmock import flexmock
|
||||
from borgmatic.hooks import dump as module
|
||||
|
||||
|
||||
def test_make_database_dump_path_joins_arguments():
|
||||
assert module.make_database_dump_path('/tmp', 'super_databases') == '/tmp/super_databases'
|
||||
def test_make_data_source_dump_path_joins_arguments():
|
||||
assert module.make_data_source_dump_path('/tmp', 'super_databases') == '/tmp/super_databases'
|
||||
|
||||
|
||||
def test_make_database_dump_path_defaults_without_source_directory():
|
||||
assert module.make_database_dump_path(None, 'super_databases') == '~/.borgmatic/super_databases'
|
||||
def test_make_data_source_dump_path_defaults_without_source_directory():
|
||||
assert (
|
||||
module.make_data_source_dump_path(None, 'super_databases') == '~/.borgmatic/super_databases'
|
||||
)
|
||||
|
||||
|
||||
def test_make_database_dump_filename_uses_name_and_hostname():
|
||||
def test_make_data_source_dump_filename_uses_name_and_hostname():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
assert (
|
||||
module.make_database_dump_filename('databases', 'test', 'hostname')
|
||||
module.make_data_source_dump_filename('databases', 'test', 'hostname')
|
||||
== 'databases/hostname/test'
|
||||
)
|
||||
|
||||
|
||||
def test_make_database_dump_filename_without_hostname_defaults_to_localhost():
|
||||
def test_make_data_source_dump_filename_without_hostname_defaults_to_localhost():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
assert module.make_database_dump_filename('databases', 'test') == 'databases/localhost/test'
|
||||
assert module.make_data_source_dump_filename('databases', 'test') == 'databases/localhost/test'
|
||||
|
||||
|
||||
def test_make_database_dump_filename_with_invalid_name_raises():
|
||||
def test_make_data_source_dump_filename_with_invalid_name_raises():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.make_database_dump_filename('databases', 'invalid/name')
|
||||
module.make_data_source_dump_filename('databases', 'invalid/name')
|
||||
|
||||
|
||||
def test_create_parent_directory_for_dump_does_not_raise():
|
||||
@@ -47,28 +49,28 @@ def test_create_named_pipe_for_dump_does_not_raise():
|
||||
module.create_named_pipe_for_dump('/path/to/pipe')
|
||||
|
||||
|
||||
def test_remove_database_dumps_removes_dump_path():
|
||||
def test_remove_data_source_dumps_removes_dump_path():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.shutil).should_receive('rmtree').with_args('databases/localhost').once()
|
||||
|
||||
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
|
||||
module.remove_data_source_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_remove_database_dumps_with_dry_run_skips_removal():
|
||||
def test_remove_data_source_dumps_with_dry_run_skips_removal():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
|
||||
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=True)
|
||||
module.remove_data_source_dumps('databases', 'SuperDB', 'test.yaml', dry_run=True)
|
||||
|
||||
|
||||
def test_remove_database_dumps_without_dump_path_present_skips_removal():
|
||||
def test_remove_data_source_dumps_without_dump_path_present_skips_removal():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('databases/localhost')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.shutil).should_receive('rmtree').never()
|
||||
|
||||
module.remove_database_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
|
||||
module.remove_data_source_dumps('databases', 'SuperDB', 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_convert_glob_patterns_to_borg_patterns_removes_leading_slash():
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import json
|
||||
|
||||
import requests
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import loki as module
|
||||
|
||||
|
||||
def test_loki_log_buffer_add_value_gets_raw():
|
||||
'''
|
||||
Assert that adding values to the log buffer increases it's length.
|
||||
'''
|
||||
buffer = module.Loki_log_buffer(flexmock(), False)
|
||||
assert len(buffer) == 0
|
||||
|
||||
buffer.add_value('Some test log line')
|
||||
assert len(buffer) == 1
|
||||
|
||||
buffer.add_value('Another test log line')
|
||||
assert len(buffer) == 2
|
||||
|
||||
|
||||
def test_loki_log_buffer_json_serializes_empty_buffer():
|
||||
'''
|
||||
Assert that the buffer correctly serializes when empty.
|
||||
'''
|
||||
buffer = module.Loki_log_buffer(flexmock(), False)
|
||||
|
||||
assert json.loads(buffer.to_request()) == json.loads('{"streams":[{"stream":{},"values":[]}]}')
|
||||
|
||||
|
||||
def test_loki_log_buffer_json_serializes_labels():
|
||||
'''
|
||||
Assert that the buffer correctly serializes with labels.
|
||||
'''
|
||||
buffer = module.Loki_log_buffer(flexmock(), False)
|
||||
buffer.add_label('test', 'label')
|
||||
|
||||
assert json.loads(buffer.to_request()) == json.loads(
|
||||
'{"streams":[{"stream":{"test": "label"},"values":[]}]}'
|
||||
)
|
||||
|
||||
|
||||
def test_loki_log_buffer_json_serializes_log_lines():
|
||||
'''
|
||||
Assert that log lines end up in the correct place in the log buffer.
|
||||
'''
|
||||
buffer = module.Loki_log_buffer(flexmock(), False)
|
||||
buffer.add_value('Some test log line')
|
||||
|
||||
assert json.loads(buffer.to_request())['streams'][0]['values'][0][1] == 'Some test log line'
|
||||
|
||||
|
||||
def test_loki_log_handler_add_label_gets_labels():
|
||||
'''
|
||||
Assert that adding labels works.
|
||||
'''
|
||||
buffer = module.Loki_log_buffer(flexmock(), False)
|
||||
|
||||
buffer.add_label('test', 'label')
|
||||
assert buffer.root['streams'][0]['stream']['test'] == 'label'
|
||||
|
||||
buffer.add_label('test2', 'label2')
|
||||
assert buffer.root['streams'][0]['stream']['test2'] == 'label2'
|
||||
|
||||
|
||||
def test_loki_log_handler_emit_gets_log_messages():
|
||||
'''
|
||||
Assert that adding log records works.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), False)
|
||||
handler.emit(flexmock(getMessage=lambda: 'Some test log line'))
|
||||
|
||||
assert len(handler.buffer) == 1
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_posts_to_server():
|
||||
'''
|
||||
Assert that the flush function sends a post request after a certain limit.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: '')
|
||||
).once()
|
||||
|
||||
for num in range(int(module.MAX_BUFFER_LINES * 1.5)):
|
||||
handler.raw(num)
|
||||
|
||||
|
||||
def test_loki_log_handler_raw_post_failure_does_not_raise():
|
||||
'''
|
||||
Assert that the flush function catches request exceptions.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), False)
|
||||
flexmock(module.requests).should_receive('post').and_return(
|
||||
flexmock(raise_for_status=lambda: (_ for _ in ()).throw(requests.RequestException()))
|
||||
).once()
|
||||
|
||||
for num in range(int(module.MAX_BUFFER_LINES * 1.5)):
|
||||
handler.raw(num)
|
||||
|
||||
|
||||
def test_loki_log_handler_flush_with_empty_buffer_does_not_raise():
|
||||
'''
|
||||
Test that flushing an empty buffer does indeed nothing.
|
||||
'''
|
||||
handler = module.Loki_log_handler(flexmock(), False)
|
||||
handler.flush()
|
||||
@@ -0,0 +1,621 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import mariadb as module
|
||||
|
||||
|
||||
def test_database_names_to_dump_passes_through_name():
|
||||
extra_environment = flexmock()
|
||||
log_prefix = ''
|
||||
|
||||
names = module.database_names_to_dump(
|
||||
{'name': 'foo'}, extra_environment, log_prefix, dry_run=False
|
||||
)
|
||||
|
||||
assert names == ('foo',)
|
||||
|
||||
|
||||
def test_database_names_to_dump_bails_for_dry_run():
|
||||
extra_environment = flexmock()
|
||||
log_prefix = ''
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').never()
|
||||
|
||||
names = module.database_names_to_dump(
|
||||
{'name': 'all'}, extra_environment, log_prefix, dry_run=True
|
||||
)
|
||||
|
||||
assert names == ()
|
||||
|
||||
|
||||
def test_database_names_to_dump_queries_mariadb_for_database_names():
|
||||
extra_environment = flexmock()
|
||||
log_prefix = ''
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
('mariadb', '--skip-column-names', '--batch', '--execute', 'show schemas'),
|
||||
extra_environment=extra_environment,
|
||||
).and_return('foo\nbar\nmysql\n').once()
|
||||
|
||||
names = module.database_names_to_dump(
|
||||
{'name': 'all'}, extra_environment, log_prefix, dry_run=False
|
||||
)
|
||||
|
||||
assert names == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
|
||||
for name, process in zip(('foo', 'bar'), processes):
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database={'name': name},
|
||||
log_prefix=object,
|
||||
dump_path=object,
|
||||
database_names=(name,),
|
||||
extra_environment=object,
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_with_password():
|
||||
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database=database,
|
||||
log_prefix=object,
|
||||
dump_path=object,
|
||||
database_names=('foo',),
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database={'name': 'all'},
|
||||
log_prefix=object,
|
||||
dump_path=object,
|
||||
database_names=('foo', 'bar'),
|
||||
extra_environment=object,
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
|
||||
databases = [{'name': 'all', 'format': 'sql'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo', 'bar'))
|
||||
|
||||
for name, process in zip(('foo', 'bar'), processes):
|
||||
flexmock(module).should_receive('execute_dump_command').with_args(
|
||||
database={'name': name, 'format': 'sql'},
|
||||
log_prefix=object,
|
||||
dump_path=object,
|
||||
database_names=(name,),
|
||||
extra_environment=object,
|
||||
dry_run=object,
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mariadb_with_list_options():
|
||||
database = {'name': 'all', 'list_options': '--defaults-extra-file=mariadb.cnf'}
|
||||
flexmock(module).should_receive('execute_command_and_capture_output').with_args(
|
||||
(
|
||||
'mariadb',
|
||||
'--defaults-extra-file=mariadb.cnf',
|
||||
'--skip-column-names',
|
||||
'--batch',
|
||||
'--execute',
|
||||
'show schemas',
|
||||
),
|
||||
extra_environment=None,
|
||||
).and_return(('foo\nbar')).once()
|
||||
|
||||
assert module.database_names_to_dump(database, None, 'test.yaml', '') == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mariadb-dump',
|
||||
'--add-drop-database',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
extra_environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo'},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mariadb-dump',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
extra_environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'add_drop_database': False},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mariadb-dump',
|
||||
'--add-drop-database',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--protocol',
|
||||
'tcp',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
extra_environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'hostname': 'database.example.org', 'port': 5433},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mariadb-dump',
|
||||
'--add-drop-database',
|
||||
'--user',
|
||||
'root',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'username': 'root', 'password': 'trustsome1'},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_with_options():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
(
|
||||
'mariadb-dump',
|
||||
'--stuff=such',
|
||||
'--add-drop-database',
|
||||
'--databases',
|
||||
'foo',
|
||||
'--result-file',
|
||||
'dump',
|
||||
),
|
||||
extra_environment=None,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo', 'options': '--stuff=such'},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=False,
|
||||
dry_run_label='',
|
||||
)
|
||||
== process
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo'},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=True,
|
||||
dry_run_label='SO DRY',
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert (
|
||||
module.execute_dump_command(
|
||||
database={'name': 'foo'},
|
||||
log_prefix='log',
|
||||
dump_path=flexmock(),
|
||||
database_names=('foo',),
|
||||
extra_environment=None,
|
||||
dry_run=True,
|
||||
dry_run_label='SO DRY',
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
('mariadb', '--batch'),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_with_options():
|
||||
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
('mariadb', '--batch', '--harder'),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_with_hostname_and_port():
|
||||
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'mariadb',
|
||||
'--batch',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
'5433',
|
||||
'--protocol',
|
||||
'tcp',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_runs_mariadb_with_username_and_password():
|
||||
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
('mariadb', '--batch', '--user', 'root'),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'restore_hostname': 'restorehost',
|
||||
'restore_port': 'restoreport',
|
||||
'restore_username': 'restoreusername',
|
||||
'restore_password': 'restorepassword',
|
||||
}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'mariadb',
|
||||
'--batch',
|
||||
'--host',
|
||||
'clihost',
|
||||
'--port',
|
||||
'cliport',
|
||||
'--protocol',
|
||||
'tcp',
|
||||
'--user',
|
||||
'cliusername',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment={'MYSQL_PWD': 'clipassword'},
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': 'clihost',
|
||||
'port': 'cliport',
|
||||
'username': 'cliusername',
|
||||
'password': 'clipassword',
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
'password': 'trustsome1',
|
||||
'hostname': 'dbhost',
|
||||
'port': 'dbport',
|
||||
'restore_username': 'restoreuser',
|
||||
'restore_password': 'restorepass',
|
||||
'restore_hostname': 'restorehost',
|
||||
'restore_port': 'restoreport',
|
||||
}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
(
|
||||
'mariadb',
|
||||
'--batch',
|
||||
'--host',
|
||||
'restorehost',
|
||||
'--port',
|
||||
'restoreport',
|
||||
'--protocol',
|
||||
'tcp',
|
||||
'--user',
|
||||
'restoreuser',
|
||||
),
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
extra_environment={'MYSQL_PWD': 'restorepass'},
|
||||
).once()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
hook_config = [{'name': 'foo'}]
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
@@ -1,53 +1,52 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import mongodb as module
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodump_for_each_database():
|
||||
def test_dump_data_sources_runs_mongodump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
for name, process in zip(('foo', 'bar'), processes):
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
['mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'],
|
||||
('mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
|
||||
|
||||
def test_dump_databases_with_dry_run_skips_mongodump():
|
||||
def test_dump_data_sources_with_dry_run_skips_mongodump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
).and_return('databases/localhost/bar')
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodump_with_hostname_and_port():
|
||||
def test_dump_data_sources_runs_mongodump_with_hostname_and_port():
|
||||
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/database.example.org/foo'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
[
|
||||
(
|
||||
'mongodump',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
@@ -58,15 +57,15 @@ def test_dump_databases_runs_mongodump_with_hostname_and_port():
|
||||
'--archive',
|
||||
'>',
|
||||
'databases/database.example.org/foo',
|
||||
],
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodump_with_username_and_password():
|
||||
def test_dump_data_sources_runs_mongodump_with_username_and_password():
|
||||
databases = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -77,13 +76,13 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
|
||||
]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
[
|
||||
(
|
||||
'mongodump',
|
||||
'--username',
|
||||
'mongo',
|
||||
@@ -96,85 +95,93 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
|
||||
'--archive',
|
||||
'>',
|
||||
'databases/localhost/foo',
|
||||
],
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodump_with_directory_format():
|
||||
def test_dump_data_sources_runs_mongodump_with_directory_format():
|
||||
databases = [{'name': 'foo', 'format': 'directory'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
['mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'],
|
||||
('mongodump', '--out', 'databases/localhost/foo', '--db', 'foo'),
|
||||
shell=True,
|
||||
).and_return(flexmock()).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodump_with_options():
|
||||
def test_dump_data_sources_runs_mongodump_with_options():
|
||||
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/foo'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
['mongodump', '--db', 'foo', '--stuff=such', '--archive', '>', 'databases/localhost/foo'],
|
||||
(
|
||||
'mongodump',
|
||||
'--db',
|
||||
'foo',
|
||||
'--stuff=such',
|
||||
'--archive',
|
||||
'>',
|
||||
'databases/localhost/foo',
|
||||
),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_databases_runs_mongodumpall_for_all_databases():
|
||||
def test_dump_data_sources_runs_mongodumpall_for_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
flexmock(module).should_receive('execute_command').with_args(
|
||||
['mongodump', '--archive', '>', 'databases/localhost/all'],
|
||||
('mongodump', '--archive', '>', 'databases/localhost/all'),
|
||||
shell=True,
|
||||
run_to_completion=False,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore():
|
||||
databases_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||
def test_restore_data_source_dump_runs_mongorestore():
|
||||
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
['mongorestore', '--archive', '--drop', '--db', 'foo'],
|
||||
['mongorestore', '--archive', '--drop'],
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -186,46 +193,19 @@ def test_restore_database_dump_runs_mongorestore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_errors_on_empty_databases_config():
|
||||
databases_config = []
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
dry_run=False,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_mongorestore_with_hostname_and_port():
|
||||
hook_config = [
|
||||
{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'mongorestore',
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--db',
|
||||
'foo',
|
||||
'--host',
|
||||
'database.example.org',
|
||||
'--port',
|
||||
@@ -236,11 +216,11 @@ def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -252,8 +232,8 @@ def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_mongorestore_with_username_and_password():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'mongo',
|
||||
@@ -265,14 +245,12 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'mongorestore',
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--db',
|
||||
'foo',
|
||||
'--username',
|
||||
'mongo',
|
||||
'--password',
|
||||
@@ -285,11 +263,11 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -301,8 +279,8 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'mongo',
|
||||
@@ -318,14 +296,12 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'mongorestore',
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--db',
|
||||
'foo',
|
||||
'--host',
|
||||
'clihost',
|
||||
'--port',
|
||||
@@ -342,11 +318,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -358,8 +334,8 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'mongo',
|
||||
@@ -375,14 +351,12 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'mongorestore',
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--db',
|
||||
'foo',
|
||||
'--host',
|
||||
'restorehost',
|
||||
'--port',
|
||||
@@ -399,11 +373,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -415,24 +389,24 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_options():
|
||||
databases_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
|
||||
def test_restore_data_source_dump_runs_mongorestore_with_options():
|
||||
hook_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
['mongorestore', '--archive', '--drop', '--db', 'foo', '--harder'],
|
||||
['mongorestore', '--archive', '--drop', '--harder'],
|
||||
processes=[extract_process],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -445,18 +419,16 @@ def test_restore_database_dump_runs_mongorestore_with_options():
|
||||
|
||||
|
||||
def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
databases_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
|
||||
hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
[
|
||||
'mongorestore',
|
||||
'--archive',
|
||||
'--drop',
|
||||
'--db',
|
||||
'foo',
|
||||
'--nsInclude',
|
||||
'bar',
|
||||
'--nsInclude',
|
||||
@@ -467,11 +439,11 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -483,12 +455,12 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
databases_config = [{'name': 'all', 'schemas': None}]
|
||||
def test_restore_data_source_dump_runs_psql_for_all_database_dump():
|
||||
hook_config = [{'name': 'all', 'schemas': None}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
['mongorestore', '--archive'],
|
||||
processes=[extract_process],
|
||||
@@ -496,11 +468,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='all',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -512,18 +484,18 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
databases_config = [{'name': 'foo', 'schemas': None}]
|
||||
def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
hook_config = [{'name': 'foo', 'schemas': None}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename')
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
@@ -535,23 +507,23 @@ def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
databases_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
|
||||
def test_restore_data_source_dump_without_extract_process_restores_from_disk():
|
||||
hook_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('/dump/path')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('/dump/path')
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
['mongorestore', '--dir', '/dump/path', '--drop', '--db', 'foo'],
|
||||
['mongorestore', '--dir', '/dump/path', '--drop'],
|
||||
processes=[],
|
||||
output_log_level=logging.DEBUG,
|
||||
input_file=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=None,
|
||||
connection_params={
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
|
||||
assert names == ('foo', 'bar')
|
||||
|
||||
|
||||
def test_dump_databases_dumps_each_database():
|
||||
def test_dump_data_sources_dumps_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
@@ -63,10 +63,10 @@ def test_dump_databases_dumps_each_database():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
|
||||
|
||||
def test_dump_databases_dumps_with_password():
|
||||
def test_dump_data_sources_dumps_with_password():
|
||||
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
@@ -84,10 +84,10 @@ def test_dump_databases_dumps_with_password():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases([database], {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources([database], {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_databases_dumps_all_databases_at_once():
|
||||
def test_dump_data_sources_dumps_all_databases_at_once():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
@@ -102,10 +102,10 @@ def test_dump_databases_dumps_all_databases_at_once():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == [process]
|
||||
|
||||
|
||||
def test_dump_databases_dumps_all_databases_separately_when_format_configured():
|
||||
def test_dump_data_sources_dumps_all_databases_separately_when_format_configured():
|
||||
databases = [{'name': 'all', 'format': 'sql'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
@@ -122,7 +122,7 @@ def test_dump_databases_dumps_all_databases_separately_when_format_configured():
|
||||
dry_run_label=object,
|
||||
).and_return(process).once()
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == processes
|
||||
|
||||
|
||||
def test_database_names_to_dump_runs_mysql_with_list_options():
|
||||
@@ -144,7 +144,7 @@ def test_database_names_to_dump_runs_mysql_with_list_options():
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -177,7 +177,7 @@ def test_execute_dump_command_runs_mysqldump():
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -209,7 +209,7 @@ def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -248,7 +248,7 @@ def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump_with_username_and_password():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -283,7 +283,7 @@ def test_execute_dump_command_runs_mysqldump_with_username_and_password():
|
||||
|
||||
def test_execute_dump_command_runs_mysqldump_with_options():
|
||||
process = flexmock()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -316,7 +316,7 @@ def test_execute_dump_command_runs_mysqldump_with_options():
|
||||
|
||||
|
||||
def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
@@ -336,7 +336,7 @@ def test_execute_dump_command_with_duplicate_dump_skips_mysqldump():
|
||||
|
||||
|
||||
def test_execute_dump_command_with_dry_run_skips_mysqldump():
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return('dump')
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return('dump')
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
||||
|
||||
@@ -356,31 +356,31 @@ def test_execute_dump_command_with_dry_run_skips_mysqldump():
|
||||
)
|
||||
|
||||
|
||||
def test_dump_databases_errors_for_missing_all_databases():
|
||||
def test_dump_data_sources_errors_for_missing_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=False)
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
def test_dump_data_sources_does_not_error_for_missing_all_databases_with_dry_run():
|
||||
databases = [{'name': 'all'}]
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'databases/localhost/all'
|
||||
)
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
assert module.dump_databases(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_to_restore():
|
||||
databases_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
def test_restore_data_source_dump_runs_mysql_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
@@ -391,11 +391,11 @@ def test_restore_database_dump_runs_mysql_to_restore():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -407,31 +407,8 @@ def test_restore_database_dump_runs_mysql_to_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_errors_when_database_missing_from_configuration():
|
||||
databases_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='other',
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_options():
|
||||
databases_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||
def test_restore_data_source_dump_runs_mysql_with_options():
|
||||
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
@@ -442,11 +419,11 @@ def test_restore_database_dump_runs_mysql_with_options():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -458,8 +435,8 @@ def test_restore_database_dump_runs_mysql_with_options():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
databases_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
def test_restore_data_source_dump_runs_mysql_with_hostname_and_port():
|
||||
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
@@ -479,11 +456,11 @@ def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -495,8 +472,8 @@ def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
databases_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
|
||||
def test_restore_data_source_dump_runs_mysql_with_username_and_password():
|
||||
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
@@ -507,11 +484,11 @@ def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -523,8 +500,8 @@ def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
@@ -556,11 +533,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
extra_environment={'MYSQL_PWD': 'clipassword'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -572,8 +549,8 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'username': 'root',
|
||||
@@ -607,11 +584,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
extra_environment={'MYSQL_PWD': 'restorepass'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -623,16 +600,16 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
databases_config = [{'name': 'foo'}]
|
||||
def test_restore_data_source_dump_with_dry_run_skips_restore():
|
||||
hook_config = [{'name': 'foo'}]
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='foo',
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
|
||||
@@ -10,8 +10,8 @@ custom_base_url = 'https://ntfy.example.com'
|
||||
topic = 'borgmatic-unit-testing'
|
||||
|
||||
custom_message_config = {
|
||||
'title': 'Borgmatic unit testing',
|
||||
'message': 'Borgmatic unit testing',
|
||||
'title': 'borgmatic unit testing',
|
||||
'message': 'borgmatic unit testing',
|
||||
'priority': 'min',
|
||||
'tags': '+1',
|
||||
}
|
||||
@@ -26,8 +26,8 @@ custom_message_headers = {
|
||||
|
||||
def return_default_message_headers(state=Enum):
|
||||
headers = {
|
||||
'X-Title': f'A Borgmatic {state.name} event happened',
|
||||
'X-Message': f'A Borgmatic {state.name} event happened',
|
||||
'X-Title': f'A borgmatic {state.name} event happened',
|
||||
'X-Message': f'A borgmatic {state.name} event happened',
|
||||
'X-Priority': 'default',
|
||||
'X-Tags': 'borgmatic',
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user