mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-23 18:33:02 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,51 @@
|
||||
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 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 +58,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
|
||||
|
||||
@@ -78,20 +78,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 +115,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)
|
||||
|
||||
@@ -142,7 +142,7 @@ 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)
|
||||
|
||||
|
||||
+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),
|
||||
)
|
||||
+11
-6
@@ -59,12 +59,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,10 +75,12 @@ 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}')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
@@ -906,8 +906,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 +919,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 +939,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 +1178,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'],
|
||||
|
||||
@@ -22,6 +22,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
|
||||
@@ -448,6 +449,16 @@ def run_actions(
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'export':
|
||||
borgmatic.actions.export_key.run_export_key(
|
||||
repository,
|
||||
config,
|
||||
local_borg_version,
|
||||
action_arguments,
|
||||
global_arguments,
|
||||
local_path,
|
||||
remote_path,
|
||||
)
|
||||
elif action_name == 'borg':
|
||||
borgmatic.actions.borg.run_borg(
|
||||
repository,
|
||||
@@ -544,9 +555,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 +576,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)
|
||||
|
||||
+204
-109
@@ -1,6 +1,8 @@
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
|
||||
import ruamel.yaml
|
||||
@@ -8,34 +10,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 +88,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 +100,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 +116,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 +128,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 +142,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)
|
||||
@@ -138,7 +182,12 @@ def load_configuration(filename):
|
||||
file_contents = file.read()
|
||||
config = yaml.load(file_contents)
|
||||
|
||||
if config and 'constants' in config:
|
||||
try:
|
||||
has_constants = bool(config and 'constants' in config)
|
||||
except TypeError:
|
||||
has_constants = False
|
||||
|
||||
if has_constants:
|
||||
for key, value in config['constants'].items():
|
||||
value = json.dumps(value)
|
||||
file_contents = file_contents.replace(f'{{{key}}}', value.strip('"'))
|
||||
@@ -149,53 +198,92 @@ def load_configuration(filename):
|
||||
return config
|
||||
|
||||
|
||||
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 +294,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 {}
|
||||
@@ -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.',
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
+180
-32
@@ -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: true
|
||||
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".
|
||||
@@ -293,7 +300,7 @@ properties:
|
||||
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 +372,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
|
||||
@@ -834,10 +841,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 +1004,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 +1017,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 +1047,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 +1132,7 @@ properties:
|
||||
type: integer
|
||||
description: |
|
||||
Port to restore to. Defaults to the "port" option.
|
||||
example: 5433
|
||||
example: 5433
|
||||
username:
|
||||
type: string
|
||||
description: |
|
||||
@@ -1026,7 +1144,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 +1191,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:
|
||||
@@ -1268,7 +1386,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
|
||||
@@ -1285,3 +1403,33 @@ properties:
|
||||
Configuration for a monitoring integration with Crunhub. 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,8 @@ from borgmatic.hooks import (
|
||||
cronhub,
|
||||
cronitor,
|
||||
healthchecks,
|
||||
loki,
|
||||
mariadb,
|
||||
mongodb,
|
||||
mysql,
|
||||
ntfy,
|
||||
@@ -18,12 +20,14 @@ HOOK_NAME_TO_MODULE = {
|
||||
'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 = ('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}')
|
||||
|
||||
+17
-15
@@ -167,7 +167,7 @@ def configure_logging(
|
||||
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
|
||||
'''
|
||||
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:
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -189,7 +189,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,46 @@ 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. 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.
|
||||
|
||||
@@ -153,7 +153,7 @@ in newer versions of borgmatic.
|
||||
Once you have multiple different configuration files, you might want to share
|
||||
common configuration options across these files with 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:
|
||||
|
||||
@@ -38,11 +38,11 @@ below for how to configure this.
|
||||
|
||||
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
|
||||
[Cronhub](https://cronhub.io), [PagerDuty](https://www.pagerduty.com/),
|
||||
[ntfy](https://ntfy.sh/), and [Grafana Loki](https://grafana.com/oss/loki/)
|
||||
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),
|
||||
@@ -50,7 +50,10 @@ hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-h
|
||||
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)
|
||||
[ntfy
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#ntfy-hook),
|
||||
and [Loki
|
||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#loki-hook),
|
||||
below for how to configure this.
|
||||
|
||||
While these services offer different features, you probably only need to use
|
||||
@@ -106,7 +109,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 +121,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 +132,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:
|
||||
@@ -144,21 +147,19 @@ healthchecks:
|
||||
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.
|
||||
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 +176,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:
|
||||
@@ -190,13 +191,9 @@ cronitor:
|
||||
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.
|
||||
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 +203,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:
|
||||
@@ -221,13 +218,9 @@ cronhub:
|
||||
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.
|
||||
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
|
||||
@@ -266,10 +259,9 @@ pagerduty:
|
||||
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.
|
||||
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 +273,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 +298,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 +322,59 @@ 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 hook in place, 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
|
||||
```
|
||||
|
||||
|
||||
## Scripting borgmatic
|
||||
|
||||
To consume the output of borgmatic in other software, you can include an
|
||||
@@ -336,7 +383,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
|
||||
|
||||
@@ -7,74 +7,69 @@ 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`.
|
||||
|
||||
|
||||
### Other ways to install
|
||||
@@ -229,7 +224,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)
|
||||
|
||||
+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
|
||||
|
||||
@@ -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`.
|
||||
@@ -21,7 +21,7 @@ apk add --no-cache python3 py3-pip borgbackup postgresql-client mariadb-client m
|
||||
py3-ruamel.yaml py3-ruamel.yaml.clib 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
|
||||
python3 -m pip install --no-cache --upgrade pip==22.2.2 setuptools==64.0.1 pymongo==4.4.1
|
||||
pip3 install --ignore-installed tox==3.25.1
|
||||
export COVERAGE_FILE=/tmp/.coverage
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.8.0'
|
||||
VERSION = '1.8.3'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -44,6 +44,14 @@ def test_load_configuration_replaces_complex_constants():
|
||||
assert module.load_configuration('config.yaml') == {'key': {'subkey': 'value'}}
|
||||
|
||||
|
||||
def test_load_configuration_with_only_integer_value_does_not_raise():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
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') == 33
|
||||
|
||||
|
||||
def test_load_configuration_inlines_include_relative_to_current_directory():
|
||||
builtins = flexmock(sys.modules['builtins'])
|
||||
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
|
||||
@@ -124,6 +132,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 +188,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 +514,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 +525,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 +704,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 +758,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 +834,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'
|
||||
|
||||
@@ -117,7 +117,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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
|
||||
@@ -748,6 +748,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 +849,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 +857,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 +869,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 +880,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 +903,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 +911,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()))
|
||||
|
||||
|
||||
@@ -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'])
|
||||
@@ -111,6 +111,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',
|
||||
(
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
+103
-127
@@ -185,7 +185,7 @@ def test_database_names_to_dump_with_all_and_psql_command_uses_custom_command():
|
||||
)
|
||||
|
||||
|
||||
def test_dump_databases_runs_pg_dump_for_each_database():
|
||||
def test_dump_data_sources_runs_pg_dump_for_each_database():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
processes = [flexmock(), flexmock()]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
@@ -193,7 +193,7 @@ def test_dump_databases_runs_pg_dump_for_each_database():
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||
('bar',)
|
||||
)
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -217,69 +217,69 @@ def test_dump_databases_runs_pg_dump_for_each_database():
|
||||
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_raises_when_no_database_names_to_dump():
|
||||
def test_dump_data_sources_raises_when_no_database_names_to_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.dump_databases(databases, {}, 'test.yaml', dry_run=False)
|
||||
module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False)
|
||||
|
||||
|
||||
def test_dump_databases_does_not_raise_when_no_database_names_to_dump():
|
||||
def test_dump_data_sources_does_not_raise_when_no_database_names_to_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||
|
||||
module.dump_databases(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
module.dump_data_sources(databases, {}, 'test.yaml', dry_run=True) == []
|
||||
|
||||
|
||||
def test_dump_databases_with_duplicate_dump_skips_pg_dump():
|
||||
def test_dump_data_sources_with_duplicate_dump_skips_pg_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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.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.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.dump_databases(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
assert module.dump_data_sources(databases, {}, 'test.yaml', dry_run=False) == []
|
||||
|
||||
|
||||
def test_dump_databases_with_dry_run_skips_pg_dump():
|
||||
def test_dump_data_sources_with_dry_run_skips_pg_dump():
|
||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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.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.os.path).should_receive('exists').and_return(False)
|
||||
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_pg_dump_with_hostname_and_port():
|
||||
def test_dump_data_sources_runs_pg_dump_with_hostname_and_port():
|
||||
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -306,10 +306,10 @@ def test_dump_databases_runs_pg_dump_with_hostname_and_port():
|
||||
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_pg_dump_with_username_and_password():
|
||||
def test_dump_data_sources_runs_pg_dump_with_username_and_password():
|
||||
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_extra_environment').and_return(
|
||||
@@ -317,7 +317,7 @@ def test_dump_databases_runs_pg_dump_with_username_and_password():
|
||||
)
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -342,15 +342,15 @@ def test_dump_databases_runs_pg_dump_with_username_and_password():
|
||||
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_pg_dump_with_directory_format():
|
||||
def test_dump_data_sources_runs_pg_dump_with_directory_format():
|
||||
databases = [{'name': 'foo', 'format': 'directory'}]
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -373,16 +373,16 @@ def test_dump_databases_runs_pg_dump_with_directory_format():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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_pg_dump_with_options():
|
||||
def test_dump_data_sources_runs_pg_dump_with_options():
|
||||
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -406,16 +406,16 @@ def test_dump_databases_runs_pg_dump_with_options():
|
||||
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_pg_dumpall_for_all_databases():
|
||||
def test_dump_data_sources_runs_pg_dumpall_for_all_databases():
|
||||
databases = [{'name': 'all'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('all',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -428,16 +428,16 @@ def test_dump_databases_runs_pg_dumpall_for_all_databases():
|
||||
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_non_default_pg_dump():
|
||||
def test_dump_data_sources_runs_non_default_pg_dump():
|
||||
databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump'}]
|
||||
process = flexmock()
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
||||
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.os.path).should_receive('exists').and_return(False)
|
||||
@@ -460,16 +460,16 @@ def test_dump_databases_runs_non_default_pg_dump():
|
||||
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_pg_restore():
|
||||
databases_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||
def test_restore_data_source_dump_runs_pg_restore():
|
||||
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -499,11 +499,11 @@ def test_restore_database_dump_runs_pg_restore():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -515,39 +515,15 @@ def test_restore_database_dump_runs_pg_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_errors_when_database_missing_from_configuration():
|
||||
databases_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
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='other',
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
'hostname': None,
|
||||
'port': None,
|
||||
'username': None,
|
||||
'password': None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_pg_restore_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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -585,11 +561,11 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -601,8 +577,8 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_username_and_password():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
|
||||
hook_config = [
|
||||
{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1', 'schemas': None}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
@@ -611,7 +587,7 @@ def test_restore_database_dump_runs_pg_restore_with_username_and_password():
|
||||
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
|
||||
)
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -645,11 +621,11 @@ def test_restore_database_dump_runs_pg_restore_with_username_and_password():
|
||||
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -661,8 +637,8 @@ def test_restore_database_dump_runs_pg_restore_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',
|
||||
'hostname': 'database.example.org',
|
||||
@@ -682,7 +658,7 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
|
||||
)
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -724,11 +700,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -740,8 +716,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',
|
||||
'hostname': 'database.example.org',
|
||||
@@ -761,7 +737,7 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
|
||||
)
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -803,11 +779,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -819,8 +795,8 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_pg_restore_with_options():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'restore_options': '--harder',
|
||||
@@ -832,7 +808,7 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -864,11 +840,11 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -880,13 +856,13 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
)
|
||||
|
||||
|
||||
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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'psql',
|
||||
@@ -903,11 +879,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='all',
|
||||
data_source={'name': 'all'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -919,13 +895,13 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
||||
databases_config = [{'name': 'foo', 'format': 'plain', 'schemas': None}]
|
||||
def test_restore_data_source_dump_runs_psql_for_plain_database_dump():
|
||||
hook_config = [{'name': 'foo', 'format': 'plain', 'schemas': None}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
('psql', '--no-password', '--no-psqlrc', '--dbname', 'foo'),
|
||||
processes=[extract_process],
|
||||
@@ -947,11 +923,11 @@ def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -963,8 +939,8 @@ def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
databases_config = [
|
||||
def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
'pg_restore_command': 'docker exec mycontainer pg_restore',
|
||||
@@ -976,7 +952,7 @@ def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'docker',
|
||||
@@ -1012,11 +988,11 @@ def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -1028,19 +1004,19 @@ def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
)
|
||||
|
||||
|
||||
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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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={
|
||||
@@ -1052,12 +1028,12 @@ 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', 'schemas': None}]
|
||||
def test_restore_data_source_dump_without_extract_process_restores_from_disk():
|
||||
hook_config = [{'name': 'foo', 'schemas': None}]
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -1088,11 +1064,11 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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={
|
||||
@@ -1104,12 +1080,12 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_schemas_restores_schemas():
|
||||
databases_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
|
||||
def test_restore_data_source_dump_with_schemas_restores_schemas():
|
||||
hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
|
||||
|
||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||
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(
|
||||
(
|
||||
'pg_restore',
|
||||
@@ -1144,11 +1120,11 @@ def test_restore_database_dump_with_schemas_restores_schemas():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).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=None,
|
||||
connection_params={
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.hooks import sqlite as module
|
||||
|
||||
|
||||
def test_dump_databases_logs_and_skips_if_dump_already_exists():
|
||||
def test_dump_data_sources_logs_and_skips_if_dump_already_exists():
|
||||
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
|
||||
flexmock(module).should_receive('execute_command').never()
|
||||
|
||||
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_dumps_each_database():
|
||||
def test_dump_data_sources_dumps_each_database():
|
||||
databases = [
|
||||
{'path': '/path/to/database1', 'name': 'database1'},
|
||||
{'path': '/path/to/database2', 'name': 'database2'},
|
||||
@@ -28,7 +27,7 @@ def test_dump_databases_dumps_each_database():
|
||||
processes = [flexmock(), flexmock()]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
@@ -37,7 +36,7 @@ def test_dump_databases_dumps_each_database():
|
||||
processes[1]
|
||||
)
|
||||
|
||||
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_dumping_database_with_non_existent_path_warns_and_dumps_database():
|
||||
@@ -48,14 +47,14 @@ def test_dumping_database_with_non_existent_path_warns_and_dumps_database():
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module.logger).should_receive('warning').once()
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
||||
|
||||
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_dumping_database_with_name_all_warns_and_dumps_all_databases():
|
||||
@@ -68,32 +67,32 @@ def test_dumping_database_with_name_all_warns_and_dumps_all_databases():
|
||||
flexmock(module.logger).should_receive(
|
||||
'warning'
|
||||
).twice() # once for the name=all, once for the non-existent path
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
||||
|
||||
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_does_not_dump_if_dry_run():
|
||||
def test_dump_data_sources_does_not_dump_if_dry_run():
|
||||
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
|
||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||
flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
|
||||
flexmock(module.dump).should_receive('make_data_source_dump_filename').and_return(
|
||||
'/path/to/dump/database'
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.dump).should_receive('create_parent_directory_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_restore_database_dump_restores_database():
|
||||
databases_config = [{'path': '/path/to/database', 'name': 'database'}, {'name': 'other'}]
|
||||
def test_restore_data_source_dump_restores_database():
|
||||
hook_config = [{'path': '/path/to/database', 'name': 'database'}, {'name': 'other'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||
@@ -108,19 +107,19 @@ def test_restore_database_dump_restores_database():
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='database',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
)
|
||||
|
||||
|
||||
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 = [
|
||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
@@ -137,19 +136,19 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='database',
|
||||
data_source={'name': 'database'},
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': 'cli/path/to/database'},
|
||||
)
|
||||
|
||||
|
||||
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 = [
|
||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
||||
]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
@@ -166,46 +165,30 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='database',
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_does_not_restore_database_if_dry_run():
|
||||
databases_config = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
def test_restore_data_source_dump_does_not_restore_database_if_dry_run():
|
||||
hook_config = [{'path': '/path/to/database', 'name': 'database'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||
flexmock(module.os).should_receive('remove').never()
|
||||
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='database',
|
||||
data_source={'name': 'database'},
|
||||
dry_run=True,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_raises_error_if_database_config_is_empty():
|
||||
databases_config = []
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
module.restore_database_dump(
|
||||
databases_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database_name='database',
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={'restore_path': None},
|
||||
)
|
||||
|
||||
+82
-70
@@ -174,16 +174,18 @@ def test_add_logging_level_skips_global_setting_if_already_set():
|
||||
module.add_logging_level('PLAID', 99)
|
||||
|
||||
|
||||
def test_configure_logging_probes_for_log_socket_on_linux():
|
||||
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_linux():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
syslog_handler = logging.handlers.SysLogHandler()
|
||||
@@ -191,19 +193,21 @@ def test_configure_logging_probes_for_log_socket_on_linux():
|
||||
address='/dev/log'
|
||||
).and_return(syslog_handler).once()
|
||||
|
||||
module.configure_logging(logging.INFO)
|
||||
module.configure_logging(logging.INFO, syslog_log_level=logging.DEBUG)
|
||||
|
||||
|
||||
def test_configure_logging_probes_for_log_socket_on_macos():
|
||||
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_macos():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(True)
|
||||
@@ -212,19 +216,21 @@ def test_configure_logging_probes_for_log_socket_on_macos():
|
||||
address='/var/run/syslog'
|
||||
).and_return(syslog_handler).once()
|
||||
|
||||
module.configure_logging(logging.INFO)
|
||||
module.configure_logging(logging.INFO, syslog_log_level=logging.DEBUG)
|
||||
|
||||
|
||||
def test_configure_logging_probes_for_log_socket_on_freebsd():
|
||||
def test_configure_logging_with_syslog_log_level_probes_for_log_socket_on_freebsd():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(False)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(False)
|
||||
@@ -234,85 +240,56 @@ def test_configure_logging_probes_for_log_socket_on_freebsd():
|
||||
address='/var/run/log'
|
||||
).and_return(syslog_handler).once()
|
||||
|
||||
module.configure_logging(logging.INFO)
|
||||
module.configure_logging(logging.INFO, syslog_log_level=logging.DEBUG)
|
||||
|
||||
|
||||
def test_configure_logging_sets_global_logger_to_most_verbose_log_level():
|
||||
def test_configure_logging_without_syslog_log_level_skips_syslog():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.DEBUG, handlers=tuple
|
||||
).once()
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
level=logging.INFO, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DEBUG)
|
||||
module.configure_logging(console_log_level=logging.INFO)
|
||||
|
||||
|
||||
def test_configure_logging_skips_syslog_if_not_found():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.INFO, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO)
|
||||
|
||||
|
||||
def test_configure_logging_skips_syslog_if_interactive_console():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module).should_receive('interactive_console').and_return(True)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO)
|
||||
|
||||
|
||||
def test_configure_logging_skips_syslog_if_syslog_logging_is_disabled():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).DISABLED = module.DISABLED
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
)
|
||||
flexmock(module).should_receive('Console_color_formatter')
|
||||
flexmock(module).should_receive('interactive_console').never()
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
|
||||
module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DISABLED)
|
||||
module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DEBUG)
|
||||
|
||||
|
||||
def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).DISABLED = module.DISABLED
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
|
||||
# syslog skipped in non-interactive console if --log-file argument provided
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.INFO, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
@@ -327,13 +304,13 @@ def test_configure_logging_to_log_file_instead_of_syslog():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
|
||||
# syslog skipped in non-interactive console if --log-file argument provided
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.DEBUG, handlers=tuple
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').never()
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
@@ -343,7 +320,40 @@ def test_configure_logging_to_log_file_instead_of_syslog():
|
||||
).and_return(file_handler).once()
|
||||
|
||||
module.configure_logging(
|
||||
console_log_level=logging.INFO, log_file_log_level=logging.DEBUG, log_file='/tmp/logfile'
|
||||
console_log_level=logging.INFO,
|
||||
syslog_log_level=logging.DISABLED,
|
||||
log_file_log_level=logging.DEBUG,
|
||||
log_file='/tmp/logfile',
|
||||
)
|
||||
|
||||
|
||||
def test_configure_logging_to_both_log_file_and_syslog():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
syslog_handler = logging.handlers.SysLogHandler()
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').with_args(
|
||||
address='/dev/log'
|
||||
).and_return(syslog_handler).once()
|
||||
file_handler = logging.handlers.WatchedFileHandler('/tmp/logfile')
|
||||
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').with_args(
|
||||
'/tmp/logfile'
|
||||
).and_return(file_handler).once()
|
||||
|
||||
module.configure_logging(
|
||||
console_log_level=logging.INFO,
|
||||
syslog_log_level=logging.DEBUG,
|
||||
log_file_log_level=logging.DEBUG,
|
||||
log_file='/tmp/logfile',
|
||||
)
|
||||
|
||||
|
||||
@@ -354,12 +364,14 @@ def test_configure_logging_to_log_file_formats_with_custom_log_format():
|
||||
'{message}', style='{' # noqa: FS003
|
||||
).once()
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.DEBUG, handlers=tuple
|
||||
level=logging.DEBUG, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||
@@ -380,13 +392,13 @@ def test_configure_logging_skips_log_file_if_argument_is_none():
|
||||
flexmock(module).should_receive('add_custom_log_levels')
|
||||
flexmock(module.logging).ANSWER = module.ANSWER
|
||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
||||
flexmock(setFormatter=lambda formatter: None, setLevel=lambda level: None)
|
||||
flexmock(
|
||||
setFormatter=lambda formatter: None, setLevel=lambda level: None, level=logging.INFO
|
||||
)
|
||||
)
|
||||
|
||||
# No WatchedFileHandler added if argument --log-file is None
|
||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||
level=logging.INFO, handlers=tuple
|
||||
level=logging.INFO, handlers=list
|
||||
)
|
||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').never()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic import signals as module
|
||||
@@ -34,6 +35,17 @@ def test_handle_signal_exits_on_sigterm():
|
||||
module.handle_signal(signal_number, frame)
|
||||
|
||||
|
||||
def test_handle_signal_raises_on_sigint():
|
||||
signal_number = module.signal.SIGINT
|
||||
frame = flexmock(f_back=flexmock(f_code=flexmock(co_name='something')))
|
||||
flexmock(module.os).should_receive('getpgrp').and_return(flexmock)
|
||||
flexmock(module.os).should_receive('killpg')
|
||||
flexmock(module.sys).should_receive('exit').never()
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
module.handle_signal(signal_number, frame)
|
||||
|
||||
|
||||
def test_configure_signals_installs_signal_handlers():
|
||||
flexmock(module.signal).should_receive('signal').at_least().once()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user