mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-23 10:33:00 +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 |
@@ -93,5 +93,3 @@ trigger:
|
||||
- borgmatic-collective/borgmatic
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
@@ -1 +1 @@
|
||||
blank_issues_enabled: false
|
||||
blank_issues_enabled: true
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
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.
|
||||
@@ -38,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,17 +78,17 @@ 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,
|
||||
)
|
||||
if config.get('store_config_files', True):
|
||||
@@ -115,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(
|
||||
|
||||
+113
-109
@@ -17,27 +17,31 @@ 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) or (None,
|
||||
None) if not found.
|
||||
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:
|
||||
try:
|
||||
@@ -47,24 +51,17 @@ def get_configured_database(
|
||||
|
||||
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,
|
||||
@@ -73,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/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'],
|
||||
@@ -107,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(
|
||||
function_name='restore_database_dump',
|
||||
function_name='restore_data_source_dump',
|
||||
config=config,
|
||||
log_prefix=repository['path'],
|
||||
hook_names=[hook_name],
|
||||
database=database,
|
||||
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,
|
||||
@@ -135,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']}
|
||||
)
|
||||
|
||||
@@ -197,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"
|
||||
)
|
||||
|
||||
|
||||
@@ -263,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
|
||||
@@ -271,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,
|
||||
)
|
||||
|
||||
@@ -291,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,
|
||||
@@ -300,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 = {
|
||||
@@ -311,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,
|
||||
@@ -333,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 databases 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,
|
||||
@@ -361,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)
|
||||
|
||||
@@ -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.
|
||||
@@ -388,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):
|
||||
|
||||
@@ -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,9 @@ 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)
|
||||
derived_match_archives = re.sub(
|
||||
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or DEFAULT_ARCHIVE_NAME_FORMAT
|
||||
)
|
||||
|
||||
if derived_match_archives == '*':
|
||||
return ()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -259,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',
|
||||
@@ -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',
|
||||
|
||||
@@ -555,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
|
||||
):
|
||||
@@ -579,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)
|
||||
|
||||
@@ -50,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.',
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1403,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,7 @@ from borgmatic.hooks import (
|
||||
cronhub,
|
||||
cronitor,
|
||||
healthchecks,
|
||||
loki,
|
||||
mariadb,
|
||||
mongodb,
|
||||
mysql,
|
||||
@@ -26,6 +27,7 @@ HOOK_NAME_TO_MODULE = {
|
||||
'pagerduty': pagerduty,
|
||||
'postgresql_databases': postgresql,
|
||||
'sqlite_databases': sqlite,
|
||||
'loki': loki,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+14
-14
@@ -6,7 +6,7 @@ from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATABASE_HOOK_NAMES = (
|
||||
DATA_SOURCE_HOOK_NAMES = (
|
||||
'mariadb_databases',
|
||||
'mysql_databases',
|
||||
'mongodb_databases',
|
||||
@@ -15,26 +15,26 @@ DATABASE_HOOK_NAMES = (
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -54,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)
|
||||
+31
-24
@@ -16,7 +16,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'), 'mariadb_databases'
|
||||
)
|
||||
|
||||
@@ -62,16 +62,17 @@ def execute_dump_command(
|
||||
):
|
||||
'''
|
||||
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 names. Use the given log prefix in any
|
||||
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 MariaDB database "{database_name}" to {dump_filename}'
|
||||
@@ -106,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 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
|
||||
@@ -165,49 +166,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), 'MariaDB', log_prefix, dry_run)
|
||||
dump.remove_data_source_dumps(make_dump_path(config), 'MariaDB', 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(
|
||||
hook_config, config, log_prefix, database, 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 a database from the given extract stream. The database is supplied as a 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.
|
||||
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 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 = (
|
||||
('mariadb', '--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 ())
|
||||
@@ -215,7 +222,7 @@ def restore_database_dump(
|
||||
)
|
||||
extra_environment = {'MYSQL_PWD': password} if password else None
|
||||
|
||||
logger.debug(f"{log_prefix}: Restoring MariaDB database {database['name']}{dry_run_label}")
|
||||
logger.debug(f"{log_prefix}: Restoring MariaDB database {data_source['name']}{dry_run_label}")
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
|
||||
+18
-18
@@ -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')
|
||||
@@ -78,46 +78,46 @@ def build_dump_command(database, dump_filename, dump_format):
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
hook_config, config, log_prefix, database, 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 a database from the given extract stream. The database is supplied as a 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.
|
||||
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 ''
|
||||
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
|
||||
|
||||
|
||||
@@ -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
-24
@@ -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,49 +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(
|
||||
hook_config, config, log_prefix, database, 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 a database from the given extract stream. The database is supplied as a 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.
|
||||
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 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 ())
|
||||
@@ -212,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
|
||||
|
||||
|
||||
@@ -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,33 +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(
|
||||
hook_config, config, log_prefix, database, 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 a database from the given extract stream. The database is supplied as a 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.
|
||||
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.
|
||||
@@ -218,54 +218,66 @@ def restore_database_dump(
|
||||
hostname, port, username, and password.
|
||||
'''
|
||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||
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
|
||||
|
||||
|
||||
+16
-16
@@ -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
|
||||
@@ -39,7 +39,7 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
||||
)
|
||||
|
||||
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,37 +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(
|
||||
hook_config, config, log_prefix, database, 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 a database from the given extract stream. The database is supplied as a 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.
|
||||
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 ''
|
||||
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)
|
||||
|
||||
@@ -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,39 +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
|
||||
|
||||
@@ -564,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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+28
-51
@@ -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
|
||||
@@ -64,45 +76,10 @@ and, if desired, replace your original configuration file with it.
|
||||
|
||||
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
|
||||
```
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
VERSION = '1.8.2'
|
||||
VERSION = '1.8.3'
|
||||
|
||||
|
||||
setup(
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
+144
-138
@@ -4,69 +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_nothing_configured():
|
||||
assert module.get_configured_database(
|
||||
def test_get_configured_data_source_matches_nothing_when_nothing_configured():
|
||||
assert module.get_configured_data_source(
|
||||
config={},
|
||||
archive_database_names={'postgresql_databases': ['foo']},
|
||||
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_configured():
|
||||
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': ['foo']},
|
||||
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_in_archive():
|
||||
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': ['bar']},
|
||||
hook_name='postgresql_databases',
|
||||
database_name='foo',
|
||||
data_source_name='foo',
|
||||
) == (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_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',
|
||||
@@ -75,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'},
|
||||
@@ -85,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',
|
||||
@@ -100,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'},
|
||||
@@ -110,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'},
|
||||
@@ -131,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'],
|
||||
}
|
||||
@@ -230,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,
|
||||
@@ -244,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,
|
||||
@@ -256,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'},
|
||||
@@ -268,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,
|
||||
@@ -289,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'],
|
||||
}
|
||||
@@ -312,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,
|
||||
@@ -342,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,
|
||||
@@ -354,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'},
|
||||
@@ -366,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,
|
||||
@@ -380,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'],
|
||||
}
|
||||
@@ -390,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,
|
||||
@@ -420,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,
|
||||
@@ -432,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'},
|
||||
@@ -444,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,
|
||||
@@ -458,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'],
|
||||
@@ -469,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,
|
||||
@@ -492,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,
|
||||
@@ -504,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'},
|
||||
@@ -516,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,
|
||||
|
||||
@@ -88,8 +88,8 @@ 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',
|
||||
(
|
||||
(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}', # noqa: FS003
|
||||
@@ -145,7 +145,12 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
|
||||
True,
|
||||
(),
|
||||
),
|
||||
(None, '{utcnow}-docs-{user}', False, ('--glob-archives', '*-docs-{user}')), # noqa: FS003
|
||||
(
|
||||
None,
|
||||
'{utcnow}-docs-{user}', # noqa: FS003
|
||||
False,
|
||||
('--glob-archives', '*-docs-{user}'), # noqa: FS003
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_make_match_archives_flags_makes_flags_with_globs(
|
||||
|
||||
@@ -849,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'))
|
||||
|
||||
@@ -857,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(
|
||||
@@ -869,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(
|
||||
@@ -880,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()))
|
||||
|
||||
@@ -889,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()))
|
||||
|
||||
@@ -897,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()))
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -44,7 +44,7 @@ def test_database_names_to_dump_queries_mariadb_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_mariadb_with_list_options():
|
||||
@@ -144,7 +144,7 @@ def test_database_names_to_dump_runs_mariadb_with_list_options():
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump():
|
||||
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_mariadb_dump():
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_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_mariadb_dump_without_add_drop_database():
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_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_mariadb_dump_with_hostname_and_port():
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_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_mariadb_dump_with_username_and_password():
|
||||
|
||||
def test_execute_dump_command_runs_mariadb_dump_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_mariadb_dump_with_options():
|
||||
|
||||
|
||||
def test_execute_dump_command_with_duplicate_dump_skips_mariadb_dump():
|
||||
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_mariadb_dump():
|
||||
|
||||
|
||||
def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
|
||||
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,30 +356,30 @@ def test_execute_dump_command_with_dry_run_skips_mariadb_dump():
|
||||
)
|
||||
|
||||
|
||||
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_mariadb_to_restore():
|
||||
def test_restore_data_source_dump_runs_mariadb_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
@@ -391,11 +391,11 @@ def test_restore_database_dump_runs_mariadb_to_restore():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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,7 +407,7 @@ def test_restore_database_dump_runs_mariadb_to_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mariadb_with_options():
|
||||
def test_restore_data_source_dump_runs_mariadb_with_options():
|
||||
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
@@ -419,11 +419,11 @@ def test_restore_database_dump_runs_mariadb_with_options():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -435,7 +435,7 @@ def test_restore_database_dump_runs_mariadb_with_options():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mariadb_with_hostname_and_port():
|
||||
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())
|
||||
|
||||
@@ -456,11 +456,11 @@ def test_restore_database_dump_runs_mariadb_with_hostname_and_port():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -472,7 +472,7 @@ def test_restore_database_dump_runs_mariadb_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mariadb_with_username_and_password():
|
||||
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())
|
||||
|
||||
@@ -484,11 +484,11 @@ def test_restore_database_dump_runs_mariadb_with_username_and_password():
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -500,7 +500,7 @@ def test_restore_database_dump_runs_mariadb_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -533,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -549,7 +549,7 @@ 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():
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -584,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -600,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():
|
||||
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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database={'name': 'foo'},
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
|
||||
@@ -5,11 +5,11 @@ 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')
|
||||
@@ -21,26 +21,26 @@ def test_dump_databases_runs_mongodump_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_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')
|
||||
@@ -62,10 +62,10 @@ def test_dump_databases_runs_mongodump_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_mongodump_with_username_and_password():
|
||||
def test_dump_data_sources_runs_mongodump_with_username_and_password():
|
||||
databases = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -76,7 +76,7 @@ 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')
|
||||
@@ -100,13 +100,13 @@ def test_dump_databases_runs_mongodump_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_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')
|
||||
@@ -117,14 +117,14 @@ def test_dump_databases_runs_mongodump_with_directory_format():
|
||||
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')
|
||||
@@ -143,14 +143,14 @@ def test_dump_databases_runs_mongodump_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_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')
|
||||
@@ -161,15 +161,15 @@ def test_dump_databases_runs_mongodumpall_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_restore_database_dump_runs_mongorestore():
|
||||
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'],
|
||||
processes=[extract_process],
|
||||
@@ -177,11 +177,11 @@ def test_restore_database_dump_runs_mongorestore():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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={
|
||||
@@ -193,14 +193,14 @@ def test_restore_database_dump_runs_mongorestore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
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',
|
||||
@@ -216,11 +216,11 @@ def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -232,7 +232,7 @@ def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
def test_restore_data_source_dump_runs_mongorestore_with_username_and_password():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -245,7 +245,7 @@ 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',
|
||||
@@ -263,11 +263,11 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -279,7 +279,7 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -296,7 +296,7 @@ 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',
|
||||
@@ -318,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -334,7 +334,7 @@ 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():
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -351,7 +351,7 @@ 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',
|
||||
@@ -373,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -389,12 +389,12 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mongorestore_with_options():
|
||||
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', '--harder'],
|
||||
processes=[extract_process],
|
||||
@@ -402,11 +402,11 @@ def test_restore_database_dump_runs_mongorestore_with_options():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -423,7 +423,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
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',
|
||||
@@ -439,11 +439,11 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -455,12 +455,12 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
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],
|
||||
@@ -468,11 +468,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
input_file=extract_process.stdout,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -484,18 +484,18 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database={'name': 'foo'},
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
@@ -507,11 +507,11 @@ def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
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'],
|
||||
processes=[],
|
||||
@@ -519,11 +519,11 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
input_file=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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,30 +356,30 @@ 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():
|
||||
def test_restore_data_source_dump_runs_mysql_to_restore():
|
||||
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
@@ -391,11 +391,11 @@ def test_restore_database_dump_runs_mysql_to_restore():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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,7 +407,7 @@ def test_restore_database_dump_runs_mysql_to_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_options():
|
||||
def test_restore_data_source_dump_runs_mysql_with_options():
|
||||
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
@@ -419,11 +419,11 @@ def test_restore_database_dump_runs_mysql_with_options():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -435,7 +435,7 @@ def test_restore_database_dump_runs_mysql_with_options():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
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())
|
||||
|
||||
@@ -456,11 +456,11 @@ def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
extra_environment=None,
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -472,7 +472,7 @@ def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
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())
|
||||
|
||||
@@ -484,11 +484,11 @@ def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -500,7 +500,7 @@ def test_restore_database_dump_runs_mysql_with_username_and_password():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -533,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(
|
||||
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={
|
||||
@@ -549,7 +549,7 @@ 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():
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -584,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -600,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():
|
||||
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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database={'name': 'foo'},
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
|
||||
@@ -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():
|
||||
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(
|
||||
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,7 +515,7 @@ def test_restore_database_dump_runs_pg_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
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}
|
||||
]
|
||||
@@ -523,7 +523,7 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
|
||||
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',
|
||||
@@ -561,11 +561,11 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -577,7 +577,7 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_username_and_password():
|
||||
def test_restore_data_source_dump_runs_pg_restore_with_username_and_password():
|
||||
hook_config = [
|
||||
{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1', 'schemas': None}
|
||||
]
|
||||
@@ -587,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',
|
||||
@@ -621,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -637,7 +637,7 @@ 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():
|
||||
def test_restore_data_source_dump_with_connection_params_uses_connection_params_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -658,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',
|
||||
@@ -700,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(
|
||||
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={
|
||||
@@ -716,7 +716,7 @@ 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():
|
||||
def test_restore_data_source_dump_without_connection_params_uses_restore_params_in_config_for_restore():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -737,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',
|
||||
@@ -779,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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -795,7 +795,7 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
def test_restore_data_source_dump_runs_pg_restore_with_options():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -808,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',
|
||||
@@ -840,11 +840,11 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -856,13 +856,13 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
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',
|
||||
@@ -879,11 +879,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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={
|
||||
@@ -895,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():
|
||||
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],
|
||||
@@ -923,11 +923,11 @@ def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -939,7 +939,7 @@ def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
def test_restore_data_source_dump_runs_non_default_pg_restore_and_psql():
|
||||
hook_config = [
|
||||
{
|
||||
'name': 'foo',
|
||||
@@ -952,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',
|
||||
@@ -988,11 +988,11 @@ def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=extract_process,
|
||||
connection_params={
|
||||
@@ -1004,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():
|
||||
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(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database={'name': 'foo'},
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=True,
|
||||
extract_process=flexmock(),
|
||||
connection_params={
|
||||
@@ -1028,12 +1028,12 @@ def test_restore_database_dump_with_dry_run_skips_restore():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
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',
|
||||
@@ -1064,11 +1064,11 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database={'name': 'foo'},
|
||||
data_source={'name': 'foo'},
|
||||
dry_run=False,
|
||||
extract_process=None,
|
||||
connection_params={
|
||||
@@ -1080,12 +1080,12 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
||||
)
|
||||
|
||||
|
||||
def test_restore_database_dump_with_schemas_restores_schemas():
|
||||
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',
|
||||
@@ -1120,11 +1120,11 @@ def test_restore_database_dump_with_schemas_restores_schemas():
|
||||
extra_environment={'PGSSLMODE': 'disable'},
|
||||
).once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
data_source=hook_config[0],
|
||||
dry_run=False,
|
||||
extract_process=None,
|
||||
connection_params={
|
||||
|
||||
@@ -5,21 +5,21 @@ 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'},
|
||||
@@ -27,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)
|
||||
@@ -36,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():
|
||||
@@ -47,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():
|
||||
@@ -67,31 +67,31 @@ 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():
|
||||
def test_restore_data_source_dump_restores_database():
|
||||
hook_config = [{'path': '/path/to/database', 'name': 'database'}, {'name': 'other'}]
|
||||
extract_process = flexmock(stdout=flexmock())
|
||||
|
||||
@@ -107,18 +107,18 @@ def test_restore_database_dump_restores_database():
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
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():
|
||||
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'}
|
||||
]
|
||||
@@ -136,18 +136,18 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
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():
|
||||
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'}
|
||||
]
|
||||
@@ -165,29 +165,29 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
||||
|
||||
flexmock(module.os).should_receive('remove').once()
|
||||
|
||||
module.restore_database_dump(
|
||||
module.restore_data_source_dump(
|
||||
hook_config,
|
||||
{},
|
||||
'test.yaml',
|
||||
database=hook_config[0],
|
||||
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():
|
||||
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(
|
||||
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},
|
||||
|
||||
+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