mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-08-06 16:23: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
|
- borgmatic-collective/borgmatic
|
||||||
branch:
|
branch:
|
||||||
- main
|
- 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
|
1.8.2
|
||||||
* #345: Add "key export" action to export a copy of the repository key for safekeeping in case
|
* #345: Add "key export" action to export a copy of the repository key for safekeeping in case
|
||||||
the original goes missing or gets damaged.
|
the original goes missing or gets damaged.
|
||||||
@@ -38,10 +58,10 @@
|
|||||||
"check --repair".
|
"check --repair".
|
||||||
* When merging two configuration files, error gracefully if the two files do not adhere to the same
|
* When merging two configuration files, error gracefully if the two files do not adhere to the same
|
||||||
format.
|
format.
|
||||||
* #721: Remove configuration sections ("location:", "storage:", "hooks:" etc.), while still keeping
|
* #721: Remove configuration sections ("location:", "storage:", "hooks:", etc.), while still
|
||||||
deprecated support for them. Now, all options are at the same level, and you don't need to worry
|
keeping deprecated support for them. Now, all options are at the same level, and you don't need
|
||||||
about commenting/uncommenting section headers when you change an option (if you remove your
|
to worry about commenting/uncommenting section headers when you change an option (if you remove
|
||||||
sections first).
|
your sections first).
|
||||||
* #721: BREAKING: The retention prefix and the consistency prefix can no longer have different
|
* #721: BREAKING: The retention prefix and the consistency prefix can no longer have different
|
||||||
values (unless one is not set).
|
values (unless one is not set).
|
||||||
* #721: BREAKING: The storage umask and the hooks umask can no longer have different values (unless
|
* #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}')
|
logger.info(f'{repository.get("label", repository["path"])}: Creating archive{dry_run_label}')
|
||||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
'remove_database_dumps',
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
active_dumps = borgmatic.hooks.dispatch.call_hooks(
|
||||||
'dump_databases',
|
'dump_data_sources',
|
||||||
config,
|
config,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
if config.get('store_config_files', True):
|
if config.get('store_config_files', True):
|
||||||
@@ -115,10 +115,10 @@ def run_create(
|
|||||||
yield json.loads(json_output)
|
yield json.loads(json_output)
|
||||||
|
|
||||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
'remove_database_dumps',
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
config_filename,
|
config_filename,
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
borgmatic.hooks.command.execute_hook(
|
borgmatic.hooks.command.execute_hook(
|
||||||
|
|||||||
+113
-109
@@ -17,27 +17,31 @@ logger = logging.getLogger(__name__)
|
|||||||
UNSPECIFIED_HOOK = object()
|
UNSPECIFIED_HOOK = object()
|
||||||
|
|
||||||
|
|
||||||
def get_configured_database(
|
def get_configured_data_source(
|
||||||
config, archive_database_names, hook_name, database_name, configuration_database_name=None
|
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
|
Find the first data source with the given hook name and data source name in the configuration
|
||||||
the given archive database names dict (from hook name to database names contained in a
|
dict and the given archive data source names dict (from hook name to data source names contained
|
||||||
particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all database
|
in a particular backup archive). If UNSPECIFIED_HOOK is given as the hook name, search all data
|
||||||
hooks for the named database. If a configuration database name is given, use that instead of the
|
source hooks for the named data source. If a configuration data source name is given, use that
|
||||||
database name to lookup the database in the given hooks configuration.
|
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,
|
Return the found data source as a tuple of (found hook name, data source configuration dict) or
|
||||||
None) if not found.
|
(None, None) if not found.
|
||||||
'''
|
'''
|
||||||
if not configuration_database_name:
|
if not configuration_data_source_name:
|
||||||
configuration_database_name = database_name
|
configuration_data_source_name = data_source_name
|
||||||
|
|
||||||
if hook_name == UNSPECIFIED_HOOK:
|
if hook_name == UNSPECIFIED_HOOK:
|
||||||
hooks_to_search = {
|
hooks_to_search = {
|
||||||
hook_name: value
|
hook_name: value
|
||||||
for (hook_name, value) in config.items()
|
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:
|
else:
|
||||||
try:
|
try:
|
||||||
@@ -47,24 +51,17 @@ def get_configured_database(
|
|||||||
|
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
(name, hook_database)
|
(name, hook_data_source)
|
||||||
for (name, hook) in hooks_to_search.items()
|
for (name, hook) in hooks_to_search.items()
|
||||||
for hook_database in hook
|
for hook_data_source in hook
|
||||||
if hook_database['name'] == configuration_database_name
|
if hook_data_source['name'] == configuration_data_source_name
|
||||||
and database_name in archive_database_names.get(name, [])
|
and data_source_name in archive_data_source_names.get(name, [])
|
||||||
),
|
),
|
||||||
(None, None),
|
(None, None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_configured_hook_name_and_database(hooks, database_name):
|
def restore_single_data_source(
|
||||||
'''
|
|
||||||
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(
|
|
||||||
repository,
|
repository,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
@@ -73,27 +70,27 @@ def restore_single_database(
|
|||||||
remote_path,
|
remote_path,
|
||||||
archive_name,
|
archive_name,
|
||||||
hook_name,
|
hook_name,
|
||||||
database,
|
data_source,
|
||||||
connection_params,
|
connection_params,
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
'''
|
'''
|
||||||
Given (among other things) an archive name, a database hook name, the hostname, port,
|
Given (among other things) an archive name, a data source hook name, the hostname, port,
|
||||||
username/password as connection params, and a configured database configuration dict, restore
|
username/password as connection params, and a configured data source configuration dict, restore
|
||||||
that database from the archive.
|
that data source from the archive.
|
||||||
'''
|
'''
|
||||||
logger.info(
|
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(
|
dump_pattern = borgmatic.hooks.dispatch.call_hooks(
|
||||||
'make_database_dump_pattern',
|
'make_data_source_dump_pattern',
|
||||||
config,
|
config,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
database['name'],
|
data_source['name'],
|
||||||
)[hook_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(
|
extract_process = borgmatic.borg.extract.extract_archive(
|
||||||
dry_run=global_arguments.dry_run,
|
dry_run=global_arguments.dry_run,
|
||||||
repository=repository['path'],
|
repository=repository['path'],
|
||||||
@@ -107,23 +104,23 @@ def restore_single_database(
|
|||||||
destination_path='/',
|
destination_path='/',
|
||||||
# A directory format dump isn't a single file, and therefore can't extract
|
# 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.
|
# 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(
|
borgmatic.hooks.dispatch.call_hooks(
|
||||||
function_name='restore_database_dump',
|
function_name='restore_data_source_dump',
|
||||||
config=config,
|
config=config,
|
||||||
log_prefix=repository['path'],
|
log_prefix=repository['path'],
|
||||||
hook_names=[hook_name],
|
hook_names=[hook_name],
|
||||||
database=database,
|
data_source=data_source,
|
||||||
dry_run=global_arguments.dry_run,
|
dry_run=global_arguments.dry_run,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params=connection_params,
|
connection_params=connection_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def collect_archive_database_names(
|
def collect_archive_data_source_names(
|
||||||
repository,
|
repository,
|
||||||
archive,
|
archive,
|
||||||
config,
|
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
|
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,
|
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
|
query the archive for the names of data sources it contains as dumps and return them as a dict
|
||||||
name to a sequence of database names.
|
from hook name to a sequence of data source names.
|
||||||
'''
|
'''
|
||||||
borgmatic_source_directory = os.path.expanduser(
|
borgmatic_source_directory = os.path.expanduser(
|
||||||
config.get(
|
config.get(
|
||||||
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
'borgmatic_source_directory', borgmatic.borg.state.DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
||||||
)
|
)
|
||||||
).lstrip('/')
|
).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(
|
dump_paths = borgmatic.borg.list.capture_archive_listing(
|
||||||
repository,
|
repository,
|
||||||
archive,
|
archive,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
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,
|
local_path=local_path,
|
||||||
remote_path=remote_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.
|
# add them to restore_names.
|
||||||
archive_database_names = {}
|
archive_data_source_names = {}
|
||||||
|
|
||||||
for dump_path in dump_paths:
|
for dump_path in dump_paths:
|
||||||
try:
|
try:
|
||||||
(hook_name, _, database_name) = dump_path.split(
|
(hook_name, _, data_source_name) = dump_path.split(
|
||||||
borgmatic_source_directory + os.path.sep, 1
|
borgmatic_source_directory + os.path.sep, 1
|
||||||
)[1].split(os.path.sep)[0:3]
|
)[1].split(os.path.sep)[0:3]
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
logger.warning(
|
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:
|
else:
|
||||||
if database_name not in archive_database_names.get(hook_name, []):
|
if data_source_name not in archive_data_source_names.get(hook_name, []):
|
||||||
archive_database_names.setdefault(hook_name, []).extend([database_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
|
Given a sequence of requested data source names to restore and a dict of hook name to the names
|
||||||
databases found in an archive, return an expanded sequence of database names to restore,
|
of data sources found in an archive, return an expanded sequence of data source names to
|
||||||
replacing "all" with actual database names as appropriate.
|
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 = (
|
restore_names = (
|
||||||
{UNSPECIFIED_HOOK: requested_database_names}
|
{UNSPECIFIED_HOOK: requested_data_source_names}
|
||||||
if requested_database_names
|
if requested_data_source_names
|
||||||
else {UNSPECIFIED_HOOK: ['all']}
|
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]:
|
if 'all' in restore_names[UNSPECIFIED_HOOK]:
|
||||||
restore_names[UNSPECIFIED_HOOK].remove('all')
|
restore_names[UNSPECIFIED_HOOK].remove('all')
|
||||||
|
|
||||||
for hook_name, database_names in archive_database_names.items():
|
for hook_name, data_source_names in archive_data_source_names.items():
|
||||||
restore_names.setdefault(hook_name, []).extend(database_names)
|
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
|
# If a data source is to be restored as part of "all", then remove it from restore names
|
||||||
# it doesn't get restored twice.
|
# so it doesn't get restored twice.
|
||||||
for database_name in database_names:
|
for data_source_name in data_source_names:
|
||||||
if database_name in restore_names[UNSPECIFIED_HOOK]:
|
if data_source_name in restore_names[UNSPECIFIED_HOOK]:
|
||||||
restore_names[UNSPECIFIED_HOOK].remove(database_name)
|
restore_names[UNSPECIFIED_HOOK].remove(data_source_name)
|
||||||
|
|
||||||
if not restore_names[UNSPECIFIED_HOOK]:
|
if not restore_names[UNSPECIFIED_HOOK]:
|
||||||
restore_names.pop(UNSPECIFIED_HOOK)
|
restore_names.pop(UNSPECIFIED_HOOK)
|
||||||
|
|
||||||
combined_restore_names = set(
|
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(
|
combined_archive_data_source_names = set(
|
||||||
name for database_names in archive_database_names.values() for name in database_names
|
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:
|
if missing_names:
|
||||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||||
raise ValueError(
|
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
|
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
|
Given a dict from hook name to data source names to restore, a dict from hook name to remaining
|
||||||
database names to restore, and a sequence of found (actually restored) database names, raise
|
data source names to restore, and a sequence of found (actually restored) data source names,
|
||||||
ValueError if requested databases to restore were missing from the archive and/or configuration.
|
raise ValueError if requested data source to restore were missing from the archive and/or
|
||||||
|
configuration.
|
||||||
'''
|
'''
|
||||||
combined_restore_names = set(
|
combined_restore_names = set(
|
||||||
name
|
name
|
||||||
for database_names in tuple(restore_names.values())
|
for data_source_names in tuple(restore_names.values())
|
||||||
+ tuple(remaining_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:
|
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))
|
missing_names = sorted(set(combined_restore_names) - set(found_names))
|
||||||
if missing_names:
|
if missing_names:
|
||||||
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
joined_names = ', '.join(f'"{name}"' for name in missing_names)
|
||||||
raise ValueError(
|
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
|
Run the "restore" action for the given repository, but only if the repository matches the
|
||||||
requested repository in restore arguments.
|
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(
|
if restore_arguments.repository and not borgmatic.config.validate.repositories_match(
|
||||||
repository, restore_arguments.repository
|
repository, restore_arguments.repository
|
||||||
@@ -271,14 +273,14 @@ def run_restore(
|
|||||||
return
|
return
|
||||||
|
|
||||||
logger.info(
|
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(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
'remove_database_dumps',
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
global_arguments.dry_run,
|
global_arguments.dry_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,7 +293,7 @@ def run_restore(
|
|||||||
local_path,
|
local_path,
|
||||||
remote_path,
|
remote_path,
|
||||||
)
|
)
|
||||||
archive_database_names = collect_archive_database_names(
|
archive_data_source_names = collect_archive_data_source_names(
|
||||||
repository['path'],
|
repository['path'],
|
||||||
archive_name,
|
archive_name,
|
||||||
config,
|
config,
|
||||||
@@ -300,7 +302,9 @@ def run_restore(
|
|||||||
local_path,
|
local_path,
|
||||||
remote_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()
|
found_names = set()
|
||||||
remaining_restore_names = {}
|
remaining_restore_names = {}
|
||||||
connection_params = {
|
connection_params = {
|
||||||
@@ -311,20 +315,20 @@ def run_restore(
|
|||||||
'restore_path': restore_arguments.restore_path,
|
'restore_path': restore_arguments.restore_path,
|
||||||
}
|
}
|
||||||
|
|
||||||
for hook_name, database_names in restore_names.items():
|
for hook_name, data_source_names in restore_names.items():
|
||||||
for database_name in database_names:
|
for data_source_name in data_source_names:
|
||||||
found_hook_name, found_database = get_configured_database(
|
found_hook_name, found_data_source = get_configured_data_source(
|
||||||
config, archive_database_names, hook_name, database_name
|
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(
|
remaining_restore_names.setdefault(found_hook_name or hook_name, []).append(
|
||||||
database_name
|
data_source_name
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_names.add(database_name)
|
found_names.add(data_source_name)
|
||||||
restore_single_database(
|
restore_single_data_source(
|
||||||
repository,
|
repository,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
@@ -333,26 +337,26 @@ def run_restore(
|
|||||||
remote_path,
|
remote_path,
|
||||||
archive_name,
|
archive_name,
|
||||||
found_hook_name or hook_name,
|
found_hook_name or hook_name,
|
||||||
dict(found_database, **{'schemas': restore_arguments.schemas}),
|
dict(found_data_source, **{'schemas': restore_arguments.schemas}),
|
||||||
connection_params,
|
connection_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
# For any databases that weren't found via exact matches in the configuration, try to fallback
|
# For any data sources that weren't found via exact matches in the configuration, try to
|
||||||
# to "all" entries.
|
# fallback to "all" entries.
|
||||||
for hook_name, database_names in remaining_restore_names.items():
|
for hook_name, data_source_names in remaining_restore_names.items():
|
||||||
for database_name in database_names:
|
for data_source_name in data_source_names:
|
||||||
found_hook_name, found_database = get_configured_database(
|
found_hook_name, found_data_source = get_configured_data_source(
|
||||||
config, archive_database_names, hook_name, database_name, 'all'
|
config, archive_data_source_names, hook_name, data_source_name, 'all'
|
||||||
)
|
)
|
||||||
|
|
||||||
if not found_database:
|
if not found_data_source:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
found_names.add(database_name)
|
found_names.add(data_source_name)
|
||||||
database = copy.copy(found_database)
|
data_source = copy.copy(found_data_source)
|
||||||
database['name'] = database_name
|
data_source['name'] = data_source_name
|
||||||
|
|
||||||
restore_single_database(
|
restore_single_data_source(
|
||||||
repository,
|
repository,
|
||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
@@ -361,16 +365,16 @@ def run_restore(
|
|||||||
remote_path,
|
remote_path,
|
||||||
archive_name,
|
archive_name,
|
||||||
found_hook_name or hook_name,
|
found_hook_name or hook_name,
|
||||||
dict(database, **{'schemas': restore_arguments.schemas}),
|
dict(data_source, **{'schemas': restore_arguments.schemas}),
|
||||||
connection_params,
|
connection_params,
|
||||||
)
|
)
|
||||||
|
|
||||||
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
|
||||||
'remove_database_dumps',
|
'remove_data_source_dumps',
|
||||||
config,
|
config,
|
||||||
repository['path'],
|
repository['path'],
|
||||||
borgmatic.hooks.dump.DATABASE_HOOK_NAMES,
|
borgmatic.hooks.dump.DATA_SOURCE_HOOK_NAMES,
|
||||||
global_arguments.dry_run,
|
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}-'
|
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):
|
def collect_borgmatic_source_directories(borgmatic_source_directory):
|
||||||
'''
|
'''
|
||||||
Return a list of borgmatic-specific source directories used for state like database backups.
|
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)
|
lock_wait = config.get('lock_wait', None)
|
||||||
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
|
list_filter_flags = make_list_filter_flags(local_borg_version, dry_run)
|
||||||
files_cache = config.get('files_cache')
|
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', '')
|
extra_borg_options = config.get('extra_borg_options', {}).get('create', '')
|
||||||
|
|
||||||
if feature.available(feature.Feature.ATIME, local_borg_version):
|
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):
|
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 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
|
return match archives flags to match archives created with the given (or default) archive name
|
||||||
any. This is done by replacing certain archive name format placeholders for ephemeral data (like
|
format. This is done by replacing certain archive name format placeholders for ephemeral data
|
||||||
"{now}") with globs.
|
(like "{now}") with globs.
|
||||||
'''
|
'''
|
||||||
if match_archives:
|
if match_archives:
|
||||||
if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
|
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:
|
else:
|
||||||
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
|
return ('--glob-archives', re.sub(r'^sh:', '', match_archives))
|
||||||
|
|
||||||
if not archive_name_format:
|
derived_match_archives = re.sub(
|
||||||
return ()
|
r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format or DEFAULT_ARCHIVE_NAME_FORMAT
|
||||||
|
)
|
||||||
derived_match_archives = re.sub(r'\{(now|utcnow|pid)([:%\w\.-]*)\}', '*', archive_name_format)
|
|
||||||
|
|
||||||
if derived_match_archives == '*':
|
if derived_match_archives == '*':
|
||||||
return ()
|
return ()
|
||||||
|
|||||||
@@ -92,13 +92,13 @@ def capture_archive_listing(
|
|||||||
config,
|
config,
|
||||||
local_borg_version,
|
local_borg_version,
|
||||||
global_arguments,
|
global_arguments,
|
||||||
list_path=None,
|
list_paths=None,
|
||||||
local_path='borg',
|
local_path='borg',
|
||||||
remote_path=None,
|
remote_path=None,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Given a local or remote repository path, an archive name, a configuration dict, the local Borg
|
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
|
local and remote Borg paths, capture the output of listing that archive and return it as a list
|
||||||
of file paths.
|
of file paths.
|
||||||
'''
|
'''
|
||||||
@@ -113,7 +113,7 @@ def capture_archive_listing(
|
|||||||
argparse.Namespace(
|
argparse.Namespace(
|
||||||
repository=repository_path,
|
repository=repository_path,
|
||||||
archive=archive,
|
archive=archive,
|
||||||
paths=[f'sh:{list_path}'],
|
paths=[f'sh:{path}' for path in list_paths] if list_paths else None,
|
||||||
find_paths=None,
|
find_paths=None,
|
||||||
json=None,
|
json=None,
|
||||||
format='{path}{NL}', # noqa: FS003
|
format='{path}{NL}', # noqa: FS003
|
||||||
|
|||||||
@@ -259,28 +259,28 @@ def make_parsers():
|
|||||||
type=int,
|
type=int,
|
||||||
choices=range(-2, 3),
|
choices=range(-2, 3),
|
||||||
default=0,
|
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(
|
global_group.add_argument(
|
||||||
'--syslog-verbosity',
|
'--syslog-verbosity',
|
||||||
type=int,
|
type=int,
|
||||||
choices=range(-2, 3),
|
choices=range(-2, 3),
|
||||||
default=0,
|
default=-2,
|
||||||
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',
|
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(
|
global_group.add_argument(
|
||||||
'--log-file-verbosity',
|
'--log-file-verbosity',
|
||||||
type=int,
|
type=int,
|
||||||
choices=range(-2, 3),
|
choices=range(-2, 3),
|
||||||
default=0,
|
default=1,
|
||||||
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',
|
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(
|
global_group.add_argument(
|
||||||
'--monitoring-verbosity',
|
'--monitoring-verbosity',
|
||||||
type=int,
|
type=int,
|
||||||
choices=range(-2, 3),
|
choices=range(-2, 3),
|
||||||
default=0,
|
default=1,
|
||||||
help='Log verbose progress to monitoring integrations that support logging (from disabled, errors only, default, some, or lots: -2, -1, 0, 1, or 2)',
|
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(
|
global_group.add_argument(
|
||||||
'--log-file',
|
'--log-file',
|
||||||
@@ -906,8 +906,8 @@ def make_parsers():
|
|||||||
restore_parser = action_parsers.add_parser(
|
restore_parser = action_parsers.add_parser(
|
||||||
'restore',
|
'restore',
|
||||||
aliases=ACTION_ALIASES['restore'],
|
aliases=ACTION_ALIASES['restore'],
|
||||||
help='Restore database dumps from a named archive',
|
help='Restore data source (e.g. database) dumps from a named archive',
|
||||||
description='Restore database dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
|
description='Restore data source (e.g. database) dumps from a named archive. (To extract files instead, use "borgmatic extract".)',
|
||||||
add_help=False,
|
add_help=False,
|
||||||
)
|
)
|
||||||
restore_group = restore_parser.add_argument_group('restore arguments')
|
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
|
'--archive', help='Name of archive to restore from (or "latest")', required=True
|
||||||
)
|
)
|
||||||
restore_group.add_argument(
|
restore_group.add_argument(
|
||||||
|
'--data-source',
|
||||||
'--database',
|
'--database',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
dest='databases',
|
dest='data_sources',
|
||||||
action='append',
|
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(
|
restore_group.add_argument(
|
||||||
'--schema',
|
'--schema',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
dest='schemas',
|
dest='schemas',
|
||||||
action='append',
|
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(
|
restore_group.add_argument(
|
||||||
'--hostname',
|
'--hostname',
|
||||||
@@ -938,7 +939,7 @@ def make_parsers():
|
|||||||
)
|
)
|
||||||
restore_group.add_argument(
|
restore_group.add_argument(
|
||||||
'--port',
|
'--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(
|
restore_group.add_argument(
|
||||||
'--username',
|
'--username',
|
||||||
|
|||||||
@@ -555,9 +555,6 @@ def log_record(suppress_log=False, **kwargs):
|
|||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
MAX_CAPTURED_OUTPUT_LENGTH = 1000
|
|
||||||
|
|
||||||
|
|
||||||
def log_error_records(
|
def log_error_records(
|
||||||
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
|
message, error=None, levelno=logging.CRITICAL, log_command_error_output=False
|
||||||
):
|
):
|
||||||
@@ -579,20 +576,24 @@ def log_error_records(
|
|||||||
raise error
|
raise error
|
||||||
except CalledProcessError as error:
|
except CalledProcessError as error:
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||||
|
|
||||||
if error.output:
|
if error.output:
|
||||||
try:
|
try:
|
||||||
output = error.output.decode('utf-8')
|
output = error.output.decode('utf-8')
|
||||||
except (UnicodeDecodeError, AttributeError):
|
except (UnicodeDecodeError, AttributeError):
|
||||||
output = error.output
|
output = error.output
|
||||||
|
|
||||||
# Suppress these logs for now and save full error output for the log summary at the end.
|
# Suppress these logs for now and save the error output for the log summary at the end.
|
||||||
yield log_record(
|
# Log a separate record per line, as some errors can be really verbose and overflow the
|
||||||
levelno=levelno,
|
# per-record size limits imposed by some logging backends.
|
||||||
levelname=level_name,
|
for output_line in output.splitlines():
|
||||||
msg=output[:MAX_CAPTURED_OUTPUT_LENGTH]
|
yield log_record(
|
||||||
+ ' ...' * (len(output) > MAX_CAPTURED_OUTPUT_LENGTH),
|
levelno=levelno,
|
||||||
suppress_log=True,
|
levelname=level_name,
|
||||||
)
|
msg=output_line,
|
||||||
|
suppress_log=True,
|
||||||
|
)
|
||||||
|
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=error)
|
yield log_record(levelno=levelno, levelname=level_name, msg=error)
|
||||||
except (ValueError, OSError) as error:
|
except (ValueError, OSError) as error:
|
||||||
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
yield log_record(levelno=levelno, levelname=level_name, msg=message)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def normalize_sections(config_filename, config):
|
|||||||
dict(
|
dict(
|
||||||
levelno=logging.WARNING,
|
levelno=logging.WARNING,
|
||||||
levelname='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
|
Configuration for a monitoring integration with Crunhub. Create an
|
||||||
account at https://cronhub.io if you'd like to use this service. See
|
account at https://cronhub.io if you'd like to use this service. See
|
||||||
borgmatic monitoring documentation for details.
|
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
|
still_running = True
|
||||||
|
|
||||||
command = process.args.split(' ') if isinstance(process.args, str) else process.args
|
command = process.args.split(' ') if isinstance(process.args, str) else process.args
|
||||||
|
|
||||||
# If any process errors, then raise accordingly.
|
# If any process errors, then raise accordingly.
|
||||||
if exit_code_indicates_error(command, exit_code, borg_local_path):
|
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
|
# 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,
|
cronhub,
|
||||||
cronitor,
|
cronitor,
|
||||||
healthchecks,
|
healthchecks,
|
||||||
|
loki,
|
||||||
mariadb,
|
mariadb,
|
||||||
mongodb,
|
mongodb,
|
||||||
mysql,
|
mysql,
|
||||||
@@ -26,6 +27,7 @@ HOOK_NAME_TO_MODULE = {
|
|||||||
'pagerduty': pagerduty,
|
'pagerduty': pagerduty,
|
||||||
'postgresql_databases': postgresql,
|
'postgresql_databases': postgresql,
|
||||||
'sqlite_databases': sqlite,
|
'sqlite_databases': sqlite,
|
||||||
|
'loki': loki,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -6,7 +6,7 @@ from borgmatic.borg.state import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATABASE_HOOK_NAMES = (
|
DATA_SOURCE_HOOK_NAMES = (
|
||||||
'mariadb_databases',
|
'mariadb_databases',
|
||||||
'mysql_databases',
|
'mysql_databases',
|
||||||
'mongodb_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
|
Given a borgmatic source directory (or None) and a data source hook name, construct a data
|
||||||
path.
|
source dump path.
|
||||||
'''
|
'''
|
||||||
if not borgmatic_source_directory:
|
if not borgmatic_source_directory:
|
||||||
borgmatic_source_directory = DEFAULT_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
|
Based on the given dump directory path, data source name, and hostname, return a filename to use
|
||||||
for the database dump. The hostname defaults to localhost.
|
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:
|
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)
|
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)
|
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
|
Remove all data source dumps in the given dump directory path (including the directory itself).
|
||||||
this is a dry run, then don't actually remove anything.
|
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 ''
|
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)
|
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.
|
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'
|
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
|
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.
|
log entries.
|
||||||
|
|
||||||
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
|
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.
|
this is a dry run, then don't actually dump anything and return None.
|
||||||
'''
|
'''
|
||||||
database_name = database['name']
|
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')
|
dump_path, database['name'], database.get('hostname')
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.exists(dump_filename):
|
if os.path.exists(dump_filename):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{log_prefix}: Skipping duplicate dump of MariaDB database "{database_name}" to {dump_filename}'
|
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
|
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
|
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]
|
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
|
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
|
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.
|
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
|
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
|
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||||
archive.
|
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(
|
def restore_data_source_dump(
|
||||||
hook_config, config, log_prefix, database, dry_run, extract_process, connection_params
|
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
|
Restore a database from the given extract stream. The database is supplied as a data source
|
||||||
dict, but the given hook configuration is ignored. The given configuration dict is used to
|
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||||
construct the destination path, and the given log prefix is used for any log entries. If this is
|
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||||
a dry run, then don't actually restore anything. Trigger the given active extract process (an
|
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||||
instance of subprocess.Popen) to produce output to consume.
|
process (an instance of subprocess.Popen) to produce output to consume.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
hostname = connection_params['hostname'] or database.get(
|
hostname = connection_params['hostname'] or data_source.get(
|
||||||
'restore_hostname', database.get('hostname')
|
'restore_hostname', data_source.get('hostname')
|
||||||
)
|
)
|
||||||
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
|
port = str(
|
||||||
username = connection_params['username'] or database.get(
|
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||||
'restore_username', database.get('username')
|
|
||||||
)
|
)
|
||||||
password = connection_params['password'] or database.get(
|
username = connection_params['username'] or data_source.get(
|
||||||
'restore_password', database.get('password')
|
'restore_username', data_source.get('username')
|
||||||
|
)
|
||||||
|
password = connection_params['password'] or data_source.get(
|
||||||
|
'restore_password', data_source.get('password')
|
||||||
)
|
)
|
||||||
|
|
||||||
restore_command = (
|
restore_command = (
|
||||||
('mariadb', '--batch')
|
('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 ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(port)) if port else ())
|
+ (('--port', str(port)) if port else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 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
|
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:
|
if dry_run:
|
||||||
return
|
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.
|
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'
|
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
|
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
|
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 = []
|
processes = []
|
||||||
for database in databases:
|
for database in databases:
|
||||||
name = database['name']
|
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')
|
make_dump_path(config), name, database.get('hostname')
|
||||||
)
|
)
|
||||||
dump_format = database.get('format', 'archive')
|
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
|
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.
|
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.
|
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,
|
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
|
and a database name to match, return the corresponding glob patterns to match the database dump
|
||||||
in an archive.
|
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(
|
def restore_data_source_dump(
|
||||||
hook_config, config, log_prefix, database, dry_run, extract_process, connection_params
|
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
|
Restore a database from the given extract stream. The database is supplied as a data source
|
||||||
dict, but the given hook configuration is ignored. The given configuration dict is used to
|
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||||
construct the destination path, and the given log prefix is used for any log entries. If this is
|
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||||
a dry run, then don't actually restore anything. Trigger the given active extract process (an
|
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||||
instance of subprocess.Popen) to produce output to consume.
|
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
|
If the extract process is None, then restore the dump from the filesystem rather than from an
|
||||||
extract stream.
|
extract stream.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
dump_filename = dump.make_database_dump_filename(
|
dump_filename = dump.make_data_source_dump_filename(
|
||||||
make_dump_path(config), database['name'], database.get('hostname')
|
make_dump_path(config), data_source['name'], data_source.get('hostname')
|
||||||
)
|
)
|
||||||
restore_command = build_restore_command(
|
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:
|
if dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy')
|
MONITOR_HOOK_NAMES = ('healthchecks', 'cronitor', 'cronhub', 'pagerduty', 'ntfy', 'loki')
|
||||||
|
|
||||||
|
|
||||||
class State(Enum):
|
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.
|
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')
|
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
|
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.
|
any log entries.
|
||||||
|
|
||||||
Return a subprocess.Popen instance for the dump process ready to spew to a named pipe. But if
|
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.
|
this is a dry run, then don't actually dump anything and return None.
|
||||||
'''
|
'''
|
||||||
database_name = database['name']
|
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')
|
dump_path, database['name'], database.get('hostname')
|
||||||
)
|
)
|
||||||
|
|
||||||
if os.path.exists(dump_filename):
|
if os.path.exists(dump_filename):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{log_prefix}: Skipping duplicate dump of MySQL database "{database_name}" to {dump_filename}'
|
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
|
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
|
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]
|
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
|
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
|
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.
|
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
|
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
|
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||||
archive.
|
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(
|
def restore_data_source_dump(
|
||||||
hook_config, config, log_prefix, database, dry_run, extract_process, connection_params
|
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
|
Restore a database from the given extract stream. The database is supplied as a data source
|
||||||
dict, but the given hook configuration is ignored. The given configuration dict is used to
|
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||||
construct the destination path, and the given log prefix is used for any log entries. If this is
|
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||||
a dry run, then don't actually restore anything. Trigger the given active extract process (an
|
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||||
instance of subprocess.Popen) to produce output to consume.
|
process (an instance of subprocess.Popen) to produce output to consume.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
hostname = connection_params['hostname'] or database.get(
|
hostname = connection_params['hostname'] or data_source.get(
|
||||||
'restore_hostname', database.get('hostname')
|
'restore_hostname', data_source.get('hostname')
|
||||||
)
|
)
|
||||||
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
|
port = str(
|
||||||
username = connection_params['username'] or database.get(
|
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||||
'restore_username', database.get('username')
|
|
||||||
)
|
)
|
||||||
password = connection_params['password'] or database.get(
|
username = connection_params['username'] or data_source.get(
|
||||||
'restore_password', database.get('password')
|
'restore_username', data_source.get('username')
|
||||||
|
)
|
||||||
|
password = connection_params['password'] or data_source.get(
|
||||||
|
'restore_password', data_source.get('password')
|
||||||
)
|
)
|
||||||
|
|
||||||
restore_command = (
|
restore_command = (
|
||||||
('mysql', '--batch')
|
('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 ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', str(port)) if port else ())
|
+ (('--port', str(port)) if port else ())
|
||||||
+ (('--protocol', 'tcp') if hostname or 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
|
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:
|
if dry_run:
|
||||||
return
|
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.
|
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'
|
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
|
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
|
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')
|
dump_format = database.get('format', None if database_name == 'all' else 'custom')
|
||||||
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
|
default_dump_command = 'pg_dumpall' if database_name == 'all' else 'pg_dump'
|
||||||
dump_command = database.get('pg_dump_command') or default_dump_command
|
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')
|
dump_path, database_name, database.get('hostname')
|
||||||
)
|
)
|
||||||
if os.path.exists(dump_filename):
|
if os.path.exists(dump_filename):
|
||||||
@@ -183,33 +183,33 @@ def dump_databases(databases, config, log_prefix, dry_run):
|
|||||||
return processes
|
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
|
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
|
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.
|
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
|
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
|
database name to match, return the corresponding glob patterns to match the database dump in an
|
||||||
archive.
|
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(
|
def restore_data_source_dump(
|
||||||
hook_config, config, log_prefix, database, dry_run, extract_process, connection_params
|
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
|
Restore a database from the given extract stream. The database is supplied as a data source
|
||||||
dict, but the given hook configuration is ignored. The given configuration dict is used to
|
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||||
construct the destination path, and the given log prefix is used for any log entries. If this is
|
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||||
a dry run, then don't actually restore anything. Trigger the given active extract process (an
|
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||||
instance of subprocess.Popen) to produce output to consume.
|
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
|
If the extract process is None, then restore the dump from the filesystem rather than from an
|
||||||
extract stream.
|
extract stream.
|
||||||
@@ -218,54 +218,66 @@ def restore_database_dump(
|
|||||||
hostname, port, username, and password.
|
hostname, port, username, and password.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
hostname = connection_params['hostname'] or database.get(
|
hostname = connection_params['hostname'] or data_source.get(
|
||||||
'restore_hostname', database.get('hostname')
|
'restore_hostname', data_source.get('hostname')
|
||||||
)
|
)
|
||||||
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
|
port = str(
|
||||||
username = connection_params['username'] or database.get(
|
connection_params['port'] or data_source.get('restore_port', data_source.get('port', ''))
|
||||||
'restore_username', database.get('username')
|
)
|
||||||
|
username = connection_params['username'] or data_source.get(
|
||||||
|
'restore_username', data_source.get('username')
|
||||||
)
|
)
|
||||||
|
|
||||||
all_databases = bool(database['name'] == 'all')
|
all_databases = bool(data_source['name'] == 'all')
|
||||||
dump_filename = dump.make_database_dump_filename(
|
dump_filename = dump.make_data_source_dump_filename(
|
||||||
make_dump_path(config), database['name'], database.get('hostname')
|
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 = (
|
analyze_command = (
|
||||||
tuple(psql_command)
|
tuple(psql_command)
|
||||||
+ ('--no-password', '--no-psqlrc', '--quiet')
|
+ ('--no-password', '--no-psqlrc', '--quiet')
|
||||||
+ (('--host', hostname) if hostname else ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', port) if port else ())
|
+ (('--port', port) if port else ())
|
||||||
+ (('--username', username) if username else ())
|
+ (('--username', username) if username else ())
|
||||||
+ (('--dbname', database['name']) if not all_databases else ())
|
+ (('--dbname', data_source['name']) if not all_databases else ())
|
||||||
+ (tuple(database['analyze_options'].split(' ')) if 'analyze_options' in database else ())
|
+ (
|
||||||
|
tuple(data_source['analyze_options'].split(' '))
|
||||||
|
if 'analyze_options' in data_source
|
||||||
|
else ()
|
||||||
|
)
|
||||||
+ ('--command', 'ANALYZE')
|
+ ('--command', 'ANALYZE')
|
||||||
)
|
)
|
||||||
use_psql_command = all_databases or database.get('format') == 'plain'
|
use_psql_command = all_databases or data_source.get('format') == 'plain'
|
||||||
pg_restore_command = shlex.split(database.get('pg_restore_command') or 'pg_restore')
|
pg_restore_command = shlex.split(data_source.get('pg_restore_command') or 'pg_restore')
|
||||||
restore_command = (
|
restore_command = (
|
||||||
tuple(psql_command if use_psql_command else pg_restore_command)
|
tuple(psql_command if use_psql_command else pg_restore_command)
|
||||||
+ ('--no-password',)
|
+ ('--no-password',)
|
||||||
+ (('--no-psqlrc',) if use_psql_command else ('--if-exists', '--exit-on-error', '--clean'))
|
+ (('--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 ())
|
+ (('--host', hostname) if hostname else ())
|
||||||
+ (('--port', port) if port else ())
|
+ (('--port', port) if port else ())
|
||||||
+ (('--username', username) if username else ())
|
+ (('--username', username) if username else ())
|
||||||
+ (('--no-owner',) if database.get('no_owner', False) else ())
|
+ (('--no-owner',) if data_source.get('no_owner', False) else ())
|
||||||
+ (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ())
|
+ (
|
||||||
|
tuple(data_source['restore_options'].split(' '))
|
||||||
|
if 'restore_options' in data_source
|
||||||
|
else ()
|
||||||
|
)
|
||||||
+ (() if extract_process else (dump_filename,))
|
+ (() if extract_process else (dump_filename,))
|
||||||
+ tuple(
|
+ tuple(
|
||||||
itertools.chain.from_iterable(('--schema', schema) for schema in database['schemas'])
|
itertools.chain.from_iterable(('--schema', schema) for schema in data_source['schemas'])
|
||||||
if database.get('schemas')
|
if data_source.get('schemas')
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
extra_environment = make_extra_environment(
|
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:
|
if dry_run:
|
||||||
return
|
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.
|
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'
|
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
|
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
|
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_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):
|
if os.path.exists(dump_filename):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f'{log_prefix}: Skipping duplicate dump of SQLite database at {database_path} to {dump_filename}'
|
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
|
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
|
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
|
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
|
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.
|
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
|
Make a pattern that matches the given SQLite3 databases. The databases are supplied as a
|
||||||
sequence of configuration dicts, as per the configuration schema.
|
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(
|
def restore_data_source_dump(
|
||||||
hook_config, config, log_prefix, database, dry_run, extract_process, connection_params
|
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
|
Restore a database from the given extract stream. The database is supplied as a data source
|
||||||
dict, but the given hook configuration is ignored. The given configuration dict is used to
|
configuration dict, but the given hook configuration is ignored. The given configuration dict is
|
||||||
construct the destination path, and the given log prefix is used for any log entries. If this is
|
used to construct the destination path, and the given log prefix is used for any log entries. If
|
||||||
a dry run, then don't actually restore anything. Trigger the given active extract process (an
|
this is a dry run, then don't actually restore anything. Trigger the given active extract
|
||||||
instance of subprocess.Popen) to produce output to consume.
|
process (an instance of subprocess.Popen) to produce output to consume.
|
||||||
'''
|
'''
|
||||||
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
|
||||||
database_path = connection_params['restore_path'] or database.get(
|
database_path = connection_params['restore_path'] or data_source.get(
|
||||||
'restore_path', database.get('path')
|
'restore_path', data_source.get('path')
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f'{log_prefix}: Restoring SQLite database at {database_path}{dry_run_label}')
|
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.
|
Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
|
||||||
'''
|
'''
|
||||||
if syslog_log_level is None:
|
if syslog_log_level is None:
|
||||||
syslog_log_level = console_log_level
|
syslog_log_level = logging.DISABLED
|
||||||
if log_file_log_level is None:
|
if log_file_log_level is None:
|
||||||
log_file_log_level = console_log_level
|
log_file_log_level = console_log_level
|
||||||
if monitoring_log_level is None:
|
if monitoring_log_level is None:
|
||||||
@@ -194,8 +194,11 @@ def configure_logging(
|
|||||||
console_handler.setFormatter(Console_color_formatter())
|
console_handler.setFormatter(Console_color_formatter())
|
||||||
console_handler.setLevel(console_log_level)
|
console_handler.setLevel(console_log_level)
|
||||||
|
|
||||||
syslog_path = None
|
handlers = [console_handler]
|
||||||
if log_file is None and syslog_log_level != logging.DISABLED:
|
|
||||||
|
if syslog_log_level != logging.DISABLED:
|
||||||
|
syslog_path = None
|
||||||
|
|
||||||
if os.path.exists('/dev/log'):
|
if os.path.exists('/dev/log'):
|
||||||
syslog_path = '/dev/log'
|
syslog_path = '/dev/log'
|
||||||
elif os.path.exists('/var/run/syslog'):
|
elif os.path.exists('/var/run/syslog'):
|
||||||
@@ -203,14 +206,15 @@ def configure_logging(
|
|||||||
elif os.path.exists('/var/run/log'):
|
elif os.path.exists('/var/run/log'):
|
||||||
syslog_path = '/var/run/log'
|
syslog_path = '/var/run/log'
|
||||||
|
|
||||||
if syslog_path and not interactive_console():
|
if syslog_path:
|
||||||
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
|
||||||
syslog_handler.setFormatter(
|
syslog_handler.setFormatter(
|
||||||
logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
|
logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
|
||||||
)
|
)
|
||||||
syslog_handler.setLevel(syslog_log_level)
|
syslog_handler.setLevel(syslog_log_level)
|
||||||
handlers = (console_handler, syslog_handler)
|
handlers.append(syslog_handler)
|
||||||
elif log_file and log_file_log_level != logging.DISABLED:
|
|
||||||
|
if log_file and log_file_log_level != logging.DISABLED:
|
||||||
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
file_handler = logging.handlers.WatchedFileHandler(log_file)
|
||||||
file_handler.setFormatter(
|
file_handler.setFormatter(
|
||||||
logging.Formatter(
|
logging.Formatter(
|
||||||
@@ -218,11 +222,9 @@ def configure_logging(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
file_handler.setLevel(log_file_log_level)
|
file_handler.setLevel(log_file_log_level)
|
||||||
handlers = (console_handler, file_handler)
|
handlers.append(file_handler)
|
||||||
else:
|
|
||||||
handlers = (console_handler,)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
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,
|
handlers=handlers,
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-2
@@ -23,12 +23,20 @@ def handle_signal(signal_number, frame):
|
|||||||
if signal_number == signal.SIGTERM:
|
if signal_number == signal.SIGTERM:
|
||||||
logger.critical('Exiting due to TERM signal')
|
logger.critical('Exiting due to TERM signal')
|
||||||
sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
|
sys.exit(EXIT_CODE_FROM_SIGNAL + signal.SIGTERM)
|
||||||
|
elif signal_number == signal.SIGINT:
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
|
||||||
|
|
||||||
def configure_signals():
|
def configure_signals():
|
||||||
'''
|
'''
|
||||||
Configure borgmatic's signal handlers to pass relevant signals through to any child processes
|
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)
|
signal.signal(signal_number, handle_signal)
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ eleventyNavigation:
|
|||||||
---
|
---
|
||||||
## Source code
|
## 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
|
```bash
|
||||||
git clone https://projects.torsion.org/borgmatic-collective/borgmatic.git
|
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
|
git clone ssh://git@projects.torsion.org:3022/borgmatic-collective/borgmatic.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, install borgmatic
|
Finally, install borgmatic
|
||||||
"[editable](https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs)"
|
"[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
|
so that you can run borgmatic actions during development to make sure your
|
||||||
changes work.
|
changes work:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd borgmatic
|
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
|
To get oriented with the borgmatic source code, have a look at the [source
|
||||||
code reference](https://torsion.org/borgmatic/docs/reference/source-code/).
|
code reference](https://torsion.org/borgmatic/docs/reference/source-code/).
|
||||||
|
|
||||||
|
|
||||||
## Automated tests
|
## Automated tests
|
||||||
|
|
||||||
Assuming you've cloned the borgmatic source code as described above, and
|
Assuming you've cloned the borgmatic source code as described above and you're
|
||||||
you're in the `borgmatic/` working copy, install tox, which is used for
|
in the `borgmatic/` working copy, install tox, which is used for setting up
|
||||||
setting up testing environments:
|
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
|
```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
|
```bash
|
||||||
tox
|
tox
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ forget to specify the section (like `location:` or `storage:`) that any option
|
|||||||
is in.
|
is in.
|
||||||
|
|
||||||
In this example, when borgmatic runs, all instances of `{user}` get replaced
|
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,
|
(And in this particular example, `{now}` doesn't get replaced with anything,
|
||||||
but gets passed directly to Borg.) After substitution, the logical result
|
but gets passed directly to Borg.) After substitution, the logical result
|
||||||
looks something like this:
|
looks something like this:
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ below for how to configure this.
|
|||||||
|
|
||||||
borgmatic integrates with monitoring services like
|
borgmatic integrates with monitoring services like
|
||||||
[Healthchecks](https://healthchecks.io/), [Cronitor](https://cronitor.io),
|
[Healthchecks](https://healthchecks.io/), [Cronitor](https://cronitor.io),
|
||||||
[Cronhub](https://cronhub.io), [PagerDuty](https://www.pagerduty.com/), and
|
[Cronhub](https://cronhub.io), [PagerDuty](https://www.pagerduty.com/),
|
||||||
[ntfy](https://ntfy.sh/) and pings these services whenever borgmatic runs.
|
[ntfy](https://ntfy.sh/), and [Grafana Loki](https://grafana.com/oss/loki/)
|
||||||
That way, you'll receive an alert when something goes wrong or (for certain
|
and pings these services whenever borgmatic runs. That way, you'll receive an
|
||||||
hooks) the service doesn't hear from borgmatic for a configured interval. See
|
alert when something goes wrong or (for certain hooks) the service doesn't
|
||||||
[Healthchecks
|
hear from borgmatic for a configured interval. See [Healthchecks
|
||||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook),
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#healthchecks-hook),
|
||||||
[Cronitor
|
[Cronitor
|
||||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronitor-hook),
|
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),
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#cronhub-hook),
|
||||||
[PagerDuty
|
[PagerDuty
|
||||||
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#pagerduty-hook),
|
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.
|
below for how to configure this.
|
||||||
|
|
||||||
While these services offer different features, you probably only need to use
|
While these services offer different features, you probably only need to use
|
||||||
@@ -129,7 +132,7 @@ especially the security information.
|
|||||||
## Healthchecks hook
|
## Healthchecks hook
|
||||||
|
|
||||||
[Healthchecks](https://healthchecks.io/) is a service that provides "instant
|
[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
|
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
|
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||||
URL" for your project. Here's an example:
|
URL" for your project. Here's an example:
|
||||||
@@ -144,21 +147,19 @@ healthchecks:
|
|||||||
this option in the `hooks:` section of your configuration.
|
this option in the `hooks:` section of your configuration.
|
||||||
|
|
||||||
With this hook in place, borgmatic pings your Healthchecks project when a
|
With this hook in place, borgmatic pings your Healthchecks project when a
|
||||||
backup begins, ends, or errors. Specifically, after the <a
|
backup begins, ends, or errors, but only when any of the `create`, `prune`,
|
||||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
`compact`, or `check` actions are run.
|
||||||
hooks</a> run, borgmatic lets Healthchecks know that it has started if any of
|
|
||||||
the `create`, `prune`, `compact`, or `check` actions are run.
|
|
||||||
|
|
||||||
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
||||||
the success after the `after_backup` hooks run and includes borgmatic logs in
|
the success and includes borgmatic logs in the payload data sent to
|
||||||
the payload data sent to Healthchecks. This means that borgmatic logs show up
|
Healthchecks. This means that borgmatic logs show up in the Healthchecks UI,
|
||||||
in the Healthchecks UI, although be aware that Healthchecks currently has a
|
although be aware that Healthchecks currently has a 10-kilobyte limit for the
|
||||||
10-kilobyte limit for the logs in each ping.
|
logs in each ping.
|
||||||
|
|
||||||
If an error occurs during any action or hook, borgmatic notifies Healthchecks
|
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
|
also tacking on logs including the error itself. But the logs are only
|
||||||
itself. But the logs are only included for errors that occur when a `create`,
|
included for errors that occur when a `create`, `prune`, `compact`, or `check`
|
||||||
`prune`, `compact`, or `check` action is run.
|
action is run.
|
||||||
|
|
||||||
You can customize the verbosity of the logs that are sent to Healthchecks with
|
You can customize the verbosity of the logs that are sent to Healthchecks with
|
||||||
borgmatic's `--monitoring-verbosity` flag. The `--list` and `--stats` flags
|
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 hook
|
||||||
|
|
||||||
[Cronitor](https://cronitor.io/) provides "Cron monitoring and uptime healthchecks
|
[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
|
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
|
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||||
API URL" for your monitor. Here's an example:
|
API URL" for your monitor. Here's an example:
|
||||||
@@ -190,13 +191,9 @@ cronitor:
|
|||||||
this option in the `hooks:` section of your configuration.
|
this option in the `hooks:` section of your configuration.
|
||||||
|
|
||||||
With this hook in place, borgmatic pings your Cronitor monitor when a backup
|
With this hook in place, borgmatic pings your Cronitor monitor when a backup
|
||||||
begins, ends, or errors. Specifically, after the <a
|
begins, ends, or errors, but only when any of the `prune`, `compact`,
|
||||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
`create`, or `check` actions are run. Then, if the actions complete
|
||||||
hooks</a> run, borgmatic lets Cronitor know that it has started if any of the
|
successfully or errors, borgmatic notifies Cronitor accordingly.
|
||||||
`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.
|
|
||||||
|
|
||||||
You can configure Cronitor to notify you by a [variety of
|
You can configure Cronitor to notify you by a [variety of
|
||||||
mechanisms](https://cronitor.io/docs/cron-job-notifications) when backups fail
|
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 hook
|
||||||
|
|
||||||
[Cronhub](https://cronhub.io/) provides "instant alerts when any of your
|
[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
|
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
|
their site, all you need to do is configure borgmatic with the unique "Ping
|
||||||
URL" for your monitor. Here's an example:
|
URL" for your monitor. Here's an example:
|
||||||
@@ -221,13 +218,9 @@ cronhub:
|
|||||||
this option in the `hooks:` section of your configuration.
|
this option in the `hooks:` section of your configuration.
|
||||||
|
|
||||||
With this hook in place, borgmatic pings your Cronhub monitor when a backup
|
With this hook in place, borgmatic pings your Cronhub monitor when a backup
|
||||||
begins, ends, or errors. Specifically, after the <a
|
begins, ends, or errors, but only when any of the `prune`, `compact`,
|
||||||
href="https://torsion.org/borgmatic/docs/how-to/add-preparation-and-cleanup-steps-to-backups/">`before_backup`
|
`create`, or `check` actions are run. Then, if the actions complete
|
||||||
hooks</a> run, borgmatic lets Cronhub know that it has started if any of the
|
successfully or errors, borgmatic notifies Cronhub accordingly.
|
||||||
`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.
|
|
||||||
|
|
||||||
Note that even though you configure borgmatic with the "start" variant of the
|
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
|
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.
|
this option in the `hooks:` section of your configuration.
|
||||||
|
|
||||||
With this hook in place, borgmatic creates a PagerDuty event for your service
|
With this hook in place, borgmatic creates a PagerDuty event for your service
|
||||||
whenever backups fail. Specifically, if an error occurs during a `create`,
|
whenever backups fail, but only when any of the `create`, `prune`, `compact`,
|
||||||
`prune`, `compact`, or `check` action, borgmatic sends an event to PagerDuty
|
or `check` actions are run. Note that borgmatic does not contact PagerDuty
|
||||||
before the `on_error` hooks run. Note that borgmatic does not contact
|
when a backup starts or when it ends without error.
|
||||||
PagerDuty when a backup starts or ends without error.
|
|
||||||
|
|
||||||
You can configure PagerDuty to notify you by a [variety of
|
You can configure PagerDuty to notify you by a [variety of
|
||||||
mechanisms](https://support.pagerduty.com/docs/notifications) when backups
|
mechanisms](https://support.pagerduty.com/docs/notifications) when backups
|
||||||
@@ -281,10 +273,12 @@ us](https://torsion.org/borgmatic/#support-and-contributing).
|
|||||||
|
|
||||||
## ntfy hook
|
## ntfy hook
|
||||||
|
|
||||||
[ntfy](https://ntfy.sh) is a free, simple, service (either hosted or self-hosted)
|
<span class="minilink minilink-addedin">New in version 1.6.3</span>
|
||||||
which offers simple pub/sub push notifications to multiple platforms including
|
[ntfy](https://ntfy.sh) is a free, simple, service (either hosted or
|
||||||
[web](https://ntfy.sh/stats), [Android](https://play.google.com/store/apps/details?id=io.heckel.ntfy)
|
self-hosted) which offers simple pub/sub push notifications to multiple
|
||||||
and [iOS](https://apps.apple.com/us/app/ntfy/id1625396347).
|
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,
|
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.
|
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.
|
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
|
## Scripting borgmatic
|
||||||
|
|
||||||
To consume the output of borgmatic in other software, you can include an
|
To consume the output of borgmatic in other software, you can include an
|
||||||
|
|||||||
@@ -7,74 +7,69 @@ eleventyNavigation:
|
|||||||
---
|
---
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Many users need to backup system files that require privileged access, so
|
### Prerequisites
|
||||||
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.
|
|
||||||
|
|
||||||
First, manually [install
|
First, [install
|
||||||
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at least
|
||||||
version 1.1. borgmatic does not install Borg automatically so as to avoid
|
version 1.1. borgmatic does not install Borg automatically so as to avoid
|
||||||
conflicts with existing Borg installations.
|
conflicts with existing Borg installations.
|
||||||
|
|
||||||
Then, download and install borgmatic as a [user site
|
Then, [install pipx](https://pypa.github.io/pipx/installation/) as the root
|
||||||
installation](https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site)
|
user (with `sudo`) to make installing borgmatic easy without impacting other
|
||||||
by running the following command:
|
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
|
```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
|
If you want to run borgmatic on a schedule with privileged access to your
|
||||||
Python 3.7+, as borgmatic does not support older versions of Python.
|
files, then you should install borgmatic as the root user by running the
|
||||||
|
following commands:
|
||||||
The next step is to ensure that borgmatic's commands available are on your
|
|
||||||
system `PATH`, so that you can run borgmatic:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo export 'PATH="$PATH:/root/.local/bin"' >> ~/.bashrc
|
sudo pipx ensurepath
|
||||||
source ~/.bashrc
|
sudo pipx install borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
This adds `/root/.local/bin` to your non-root user's system `PATH`.
|
Check whether this worked with:
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo borgmatic --version
|
sudo su -
|
||||||
|
borgmatic --version
|
||||||
```
|
```
|
||||||
|
|
||||||
If borgmatic is properly installed, that should output your borgmatic version.
|
If borgmatic is properly installed, that should output your borgmatic version.
|
||||||
|
And if you'd also like `sudo borgmatic` to work, keep reading!
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
### Global install option
|
### Non-root install
|
||||||
|
|
||||||
If you try the user site installation above and have problems making borgmatic
|
If you only want to run borgmatic as a non-root user (without privileged file
|
||||||
commands runnable on your system `PATH`, an alternate approach is to install
|
access) *or* you want to make `sudo borgmatic` work so borgmatic runs as root,
|
||||||
borgmatic globally.
|
then install borgmatic as a non-root user by running the following commands as
|
||||||
|
that user:
|
||||||
The following uninstalls borgmatic and then reinstalls it such that borgmatic
|
|
||||||
commands are on the default system `PATH`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 uninstall borgmatic
|
pipx ensurepath
|
||||||
sudo pip3 install --upgrade borgmatic
|
pipx install borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
The main downside of a global install is that borgmatic is less cleanly
|
This should work even if you've also installed borgmatic as the root user.
|
||||||
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
|
Check whether this worked with:
|
||||||
on a relatively dedicated system, then a global install can work out fine.
|
|
||||||
|
```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
|
### Other ways to install
|
||||||
|
|||||||
+28
-51
@@ -7,26 +7,38 @@ eleventyNavigation:
|
|||||||
---
|
---
|
||||||
## Upgrading borgmatic
|
## Upgrading borgmatic
|
||||||
|
|
||||||
In general, all you should need to do to upgrade borgmatic is run the
|
In general, all you should need to do to upgrade borgmatic if you've
|
||||||
following:
|
[installed it with
|
||||||
|
pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation)
|
||||||
|
is to run the following:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 install --user --upgrade borgmatic
|
sudo pipx upgrade borgmatic
|
||||||
```
|
```
|
||||||
|
|
||||||
See below about special cases with old versions of borgmatic. Additionally, if
|
Omit `sudo` if you installed borgmatic as a non-root user. And if you
|
||||||
you installed borgmatic [without using `pip3 install
|
installed borgmatic *both* as root and as a non-root user, you'll need to
|
||||||
--user`](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#other-ways-to-install),
|
upgrade each installation independently.
|
||||||
then your upgrade process may be different.
|
|
||||||
|
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
|
### Upgrading your configuration
|
||||||
|
|
||||||
The borgmatic configuration file format is almost always backwards-compatible
|
The borgmatic configuration file format is usually backwards-compatible from
|
||||||
from release to release without any changes, but you may still want to update
|
release to release without any changes, but you may still want to update your
|
||||||
your configuration file when you upgrade to take advantage of new
|
configuration file when you upgrade to take advantage of new configuration
|
||||||
configuration options. This is completely optional. If you prefer, you can add
|
options or avoid old configuration from eventually becoming unsupported. If
|
||||||
new configuration options manually.
|
you prefer, you can add new configuration options manually.
|
||||||
|
|
||||||
If you do want to upgrade your configuration file to include new options, use
|
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
|
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
|
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
|
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
|
to express lists of values. Modern versions of borgmatic no longer include
|
||||||
upgrade to the last version of borgmatic to support converting configuration:
|
support for upgrading configuration files this old, but feel free to [file a
|
||||||
borgmatic 1.7.14.
|
ticket](https://torsion.org/borgmatic/#issues) for help with upgrading any old
|
||||||
|
INI-style configuration files you may have.
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Upgrading Borg
|
## Upgrading Borg
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
VERSION = '1.8.2'
|
VERSION = '1.8.3'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
|||||||
global_arguments = arguments['global']
|
global_arguments = arguments['global']
|
||||||
assert global_arguments.config_paths == config_paths
|
assert global_arguments.config_paths == config_paths
|
||||||
assert global_arguments.verbosity == 0
|
assert global_arguments.verbosity == 0
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == -2
|
||||||
assert global_arguments.log_file_verbosity == 0
|
assert global_arguments.log_file_verbosity == 1
|
||||||
|
assert global_arguments.monitoring_verbosity == 1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_multiple_config_flags_parses_as_list():
|
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']
|
global_arguments = arguments['global']
|
||||||
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
assert global_arguments.config_paths == ['myconfig', 'otherconfig']
|
||||||
assert global_arguments.verbosity == 0
|
assert global_arguments.verbosity == 0
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == -2
|
||||||
assert global_arguments.log_file_verbosity == 0
|
assert global_arguments.log_file_verbosity == 1
|
||||||
|
assert global_arguments.monitoring_verbosity == 1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_action_after_config_path_omits_action():
|
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']
|
global_arguments = arguments['global']
|
||||||
assert global_arguments.config_paths == config_paths
|
assert global_arguments.config_paths == config_paths
|
||||||
assert global_arguments.verbosity == 1
|
assert global_arguments.verbosity == 1
|
||||||
assert global_arguments.syslog_verbosity == 0
|
assert global_arguments.syslog_verbosity == -2
|
||||||
assert global_arguments.log_file_verbosity == 0
|
assert global_arguments.log_file_verbosity == 1
|
||||||
|
assert global_arguments.monitoring_verbosity == 1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_syslog_verbosity_overrides_default():
|
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.config_paths == config_paths
|
||||||
assert global_arguments.verbosity == 0
|
assert global_arguments.verbosity == 0
|
||||||
assert global_arguments.syslog_verbosity == 2
|
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():
|
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']
|
global_arguments = arguments['global']
|
||||||
assert global_arguments.config_paths == config_paths
|
assert global_arguments.config_paths == config_paths
|
||||||
assert global_arguments.verbosity == 0
|
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.log_file_verbosity == -1
|
||||||
|
assert global_arguments.monitoring_verbosity == 1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_single_override_parses():
|
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
|
import borgmatic.actions.restore as module
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_matches_database_by_name():
|
def test_get_configured_data_source_matches_data_source_by_name():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={
|
config={
|
||||||
'other_databases': [{'name': 'other'}],
|
'other_databases': [{'name': 'other'}],
|
||||||
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
'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',
|
hook_name='postgresql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
) == ('postgresql_databases', {'name': 'bar'})
|
) == ('postgresql_databases', {'name': 'bar'})
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_matches_nothing_when_nothing_configured():
|
def test_get_configured_data_source_matches_nothing_when_nothing_configured():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={},
|
config={},
|
||||||
archive_database_names={'postgresql_databases': ['foo']},
|
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='quux',
|
data_source_name='quux',
|
||||||
) == (None, None)
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_matches_nothing_when_database_name_not_configured():
|
def test_get_configured_data_source_matches_nothing_when_data_source_name_not_configured():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||||
archive_database_names={'postgresql_databases': ['foo']},
|
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='quux',
|
data_source_name='quux',
|
||||||
) == (None, None)
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_matches_nothing_when_database_name_not_in_archive():
|
def test_get_configured_data_source_matches_nothing_when_data_source_name_not_in_archive():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
config={'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}]},
|
||||||
archive_database_names={'postgresql_databases': ['bar']},
|
archive_data_source_names={'postgresql_databases': ['bar']},
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='foo',
|
data_source_name='foo',
|
||||||
) == (None, None)
|
) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_matches_database_by_configuration_database_name():
|
def test_get_configured_data_source_matches_data_source_by_configuration_data_source_name():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={'postgresql_databases': [{'name': 'all'}, {'name': 'bar'}]},
|
config={'postgresql_databases': [{'name': 'all'}, {'name': 'bar'}]},
|
||||||
archive_database_names={'postgresql_databases': ['foo']},
|
archive_data_source_names={'postgresql_databases': ['foo']},
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='foo',
|
data_source_name='foo',
|
||||||
configuration_database_name='all',
|
configuration_data_source_name='all',
|
||||||
) == ('postgresql_databases', {'name': 'all'})
|
) == ('postgresql_databases', {'name': 'all'})
|
||||||
|
|
||||||
|
|
||||||
def test_get_configured_database_with_unspecified_hook_matches_database_by_name():
|
def test_get_configured_data_source_with_unspecified_hook_matches_data_source_by_name():
|
||||||
assert module.get_configured_database(
|
assert module.get_configured_data_source(
|
||||||
config={
|
config={
|
||||||
'other_databases': [{'name': 'other'}],
|
'other_databases': [{'name': 'other'}],
|
||||||
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
|
'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,
|
hook_name=module.UNSPECIFIED_HOOK,
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
) == ('postgresql_databases', {'name': 'bar'})
|
) == ('postgresql_databases', {'name': 'bar'})
|
||||||
|
|
||||||
|
|
||||||
def test_collect_archive_database_names_parses_archive_paths():
|
def test_collect_archive_data_source_names_parses_archive_paths():
|
||||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
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(
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
[
|
[
|
||||||
'.borgmatic/postgresql_databases/localhost/foo',
|
'.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'},
|
repository={'path': 'repo'},
|
||||||
archive='archive',
|
archive='archive',
|
||||||
config={'borgmatic_source_directory': '.borgmatic'},
|
config={'borgmatic_source_directory': '.borgmatic'},
|
||||||
@@ -85,14 +87,16 @@ def test_collect_archive_database_names_parses_archive_paths():
|
|||||||
remote_path=flexmock(),
|
remote_path=flexmock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert archive_database_names == {
|
assert archive_data_source_names == {
|
||||||
'postgresql_databases': ['foo', 'bar'],
|
'postgresql_databases': ['foo', 'bar'],
|
||||||
'mysql_databases': ['quux'],
|
'mysql_databases': ['quux'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
def test_collect_archive_data_source_names_parses_directory_format_archive_paths():
|
||||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
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(
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
[
|
[
|
||||||
'.borgmatic/postgresql_databases/localhost/foo/table1',
|
'.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'},
|
repository={'path': 'repo'},
|
||||||
archive='archive',
|
archive='archive',
|
||||||
config={'borgmatic_source_directory': '.borgmatic'},
|
config={'borgmatic_source_directory': '.borgmatic'},
|
||||||
@@ -110,18 +114,20 @@ def test_collect_archive_database_names_parses_directory_format_archive_paths():
|
|||||||
remote_path=flexmock(),
|
remote_path=flexmock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert archive_database_names == {
|
assert archive_data_source_names == {
|
||||||
'postgresql_databases': ['foo'],
|
'postgresql_databases': ['foo'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_collect_archive_database_names_skips_bad_archive_paths():
|
def test_collect_archive_data_source_names_skips_bad_archive_paths():
|
||||||
flexmock(module.borgmatic.hooks.dump).should_receive('make_database_dump_path').and_return('')
|
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(
|
flexmock(module.borgmatic.borg.list).should_receive('capture_archive_listing').and_return(
|
||||||
['.borgmatic/postgresql_databases/localhost/foo', '.borgmatic/invalid', 'invalid/as/well']
|
['.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'},
|
repository={'path': 'repo'},
|
||||||
archive='archive',
|
archive='archive',
|
||||||
config={'borgmatic_source_directory': '.borgmatic'},
|
config={'borgmatic_source_directory': '.borgmatic'},
|
||||||
@@ -131,96 +137,96 @@ def test_collect_archive_database_names_skips_bad_archive_paths():
|
|||||||
remote_path=flexmock(),
|
remote_path=flexmock(),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert archive_database_names == {
|
assert archive_data_source_names == {
|
||||||
'postgresql_databases': ['foo'],
|
'postgresql_databases': ['foo'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_find_databases_to_restore_passes_through_requested_names_found_in_archive():
|
def test_find_data_sources_to_restore_passes_through_requested_names_found_in_archive():
|
||||||
restore_names = module.find_databases_to_restore(
|
restore_names = module.find_data_sources_to_restore(
|
||||||
requested_database_names=['foo', 'bar'],
|
requested_data_source_names=['foo', 'bar'],
|
||||||
archive_database_names={'postresql_databases': ['foo', 'bar', 'baz']},
|
archive_data_source_names={'postresql_databases': ['foo', 'bar', 'baz']},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert restore_names == {module.UNSPECIFIED_HOOK: ['foo', 'bar']}
|
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):
|
with pytest.raises(ValueError):
|
||||||
module.find_databases_to_restore(
|
module.find_data_sources_to_restore(
|
||||||
requested_database_names=['foo', 'bar'],
|
requested_data_source_names=['foo', 'bar'],
|
||||||
archive_database_names={'postresql_databases': ['foo']},
|
archive_data_source_names={'postresql_databases': ['foo']},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_find_databases_to_restore_without_requested_names_finds_all_archive_databases():
|
def test_find_data_sources_to_restore_without_requested_names_finds_all_archive_data_sources():
|
||||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
restore_names = module.find_databases_to_restore(
|
restore_names = module.find_data_sources_to_restore(
|
||||||
requested_database_names=[],
|
requested_data_source_names=[],
|
||||||
archive_database_names=archive_database_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():
|
def test_find_data_sources_to_restore_with_all_in_requested_names_finds_all_archive_data_sources():
|
||||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
restore_names = module.find_databases_to_restore(
|
restore_names = module.find_data_sources_to_restore(
|
||||||
requested_database_names=['all'],
|
requested_data_source_names=['all'],
|
||||||
archive_database_names=archive_database_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_plus_additional_requested_names_omits_duplicates():
|
def test_find_data_sources_to_restore_with_all_in_requested_names_plus_additional_requested_names_omits_duplicates():
|
||||||
archive_database_names = {'postresql_databases': ['foo', 'bar']}
|
archive_data_source_names = {'postresql_databases': ['foo', 'bar']}
|
||||||
|
|
||||||
restore_names = module.find_databases_to_restore(
|
restore_names = module.find_data_sources_to_restore(
|
||||||
requested_database_names=['all', 'foo', 'bar'],
|
requested_data_source_names=['all', 'foo', 'bar'],
|
||||||
archive_database_names=archive_database_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_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):
|
with pytest.raises(ValueError):
|
||||||
module.find_databases_to_restore(
|
module.find_data_sources_to_restore(
|
||||||
requested_database_names=['all', 'foo', 'bar'],
|
requested_data_source_names=['all', 'foo', 'bar'],
|
||||||
archive_database_names={'postresql_databases': ['foo']},
|
archive_data_source_names={'postresql_databases': ['foo']},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_databases_found_with_all_databases_found_does_not_raise():
|
def test_ensure_data_sources_found_with_all_data_sources_found_does_not_raise():
|
||||||
module.ensure_databases_found(
|
module.ensure_data_sources_found(
|
||||||
restore_names={'postgresql_databases': ['foo']},
|
restore_names={'postgresql_databases': ['foo']},
|
||||||
remaining_restore_names={'postgresql_databases': ['bar']},
|
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||||
found_names=['foo', '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):
|
with pytest.raises(ValueError):
|
||||||
module.ensure_databases_found(
|
module.ensure_data_sources_found(
|
||||||
restore_names={'postgresql_databases': []},
|
restore_names={'postgresql_databases': []},
|
||||||
remaining_restore_names={},
|
remaining_restore_names={},
|
||||||
found_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):
|
with pytest.raises(ValueError):
|
||||||
module.ensure_databases_found(
|
module.ensure_data_sources_found(
|
||||||
restore_names={'postgresql_databases': ['foo']},
|
restore_names={'postgresql_databases': ['foo']},
|
||||||
remaining_restore_names={'postgresql_databases': ['bar']},
|
remaining_restore_names={'postgresql_databases': ['bar']},
|
||||||
found_names=['foo'],
|
found_names=['foo'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_run_restore_restores_each_database():
|
def test_run_restore_restores_each_data_source():
|
||||||
restore_names = {
|
restore_names = {
|
||||||
'postgresql_databases': ['foo', 'bar'],
|
'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(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
flexmock()
|
flexmock()
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||||
flexmock(module).should_receive('get_configured_database').and_return(
|
flexmock(module).should_receive('get_configured_data_source').and_return(
|
||||||
('postgresql_databases', {'name': 'foo'})
|
('postgresql_databases', {'name': 'foo'})
|
||||||
).and_return(('postgresql_databases', {'name': 'bar'}))
|
).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,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -244,10 +250,10 @@ def test_run_restore_restores_each_database():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'foo', 'schemas': None},
|
data_source={'name': 'foo', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('restore_single_database').with_args(
|
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||||
repository=object,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -256,10 +262,10 @@ def test_run_restore_restores_each_database():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'bar', 'schemas': None},
|
data_source={'name': 'bar', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('ensure_databases_found')
|
flexmock(module).should_receive('ensure_data_sources_found')
|
||||||
|
|
||||||
module.run_restore(
|
module.run_restore(
|
||||||
repository={'path': 'repo'},
|
repository={'path': 'repo'},
|
||||||
@@ -268,7 +274,7 @@ def test_run_restore_restores_each_database():
|
|||||||
restore_arguments=flexmock(
|
restore_arguments=flexmock(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
archive='archive',
|
archive='archive',
|
||||||
databases=flexmock(),
|
data_sources=flexmock(),
|
||||||
schemas=None,
|
schemas=None,
|
||||||
hostname=None,
|
hostname=None,
|
||||||
port=None,
|
port=None,
|
||||||
@@ -289,20 +295,20 @@ def test_run_restore_bails_for_non_matching_repository():
|
|||||||
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
flexmock(module.borgmatic.hooks.dispatch).should_receive(
|
||||||
'call_hooks_even_if_unconfigured'
|
'call_hooks_even_if_unconfigured'
|
||||||
).never()
|
).never()
|
||||||
flexmock(module).should_receive('restore_single_database').never()
|
flexmock(module).should_receive('restore_single_data_source').never()
|
||||||
|
|
||||||
module.run_restore(
|
module.run_restore(
|
||||||
repository={'path': 'repo'},
|
repository={'path': 'repo'},
|
||||||
config=flexmock(),
|
config=flexmock(),
|
||||||
local_borg_version=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),
|
global_arguments=flexmock(dry_run=False),
|
||||||
local_path=flexmock(),
|
local_path=flexmock(),
|
||||||
remote_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 = {
|
restore_names = {
|
||||||
'postgresql_databases': ['foo', 'bar'],
|
'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(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
flexmock()
|
flexmock()
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||||
flexmock(module).should_receive('get_configured_database').with_args(
|
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||||
config=object,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='foo',
|
data_source_name='foo',
|
||||||
).and_return(('postgresql_databases', {'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,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
).and_return((None, None))
|
).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,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
configuration_database_name='all',
|
configuration_data_source_name='all',
|
||||||
).and_return(('postgresql_databases', {'name': 'bar'}))
|
).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,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -342,10 +348,10 @@ def test_run_restore_restores_database_configured_with_all_name():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'foo', 'schemas': None},
|
data_source={'name': 'foo', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('restore_single_database').with_args(
|
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||||
repository=object,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -354,10 +360,10 @@ def test_run_restore_restores_database_configured_with_all_name():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'bar', 'schemas': None},
|
data_source={'name': 'bar', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('ensure_databases_found')
|
flexmock(module).should_receive('ensure_data_sources_found')
|
||||||
|
|
||||||
module.run_restore(
|
module.run_restore(
|
||||||
repository={'path': 'repo'},
|
repository={'path': 'repo'},
|
||||||
@@ -366,7 +372,7 @@ def test_run_restore_restores_database_configured_with_all_name():
|
|||||||
restore_arguments=flexmock(
|
restore_arguments=flexmock(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
archive='archive',
|
archive='archive',
|
||||||
databases=flexmock(),
|
data_sources=flexmock(),
|
||||||
schemas=None,
|
schemas=None,
|
||||||
hostname=None,
|
hostname=None,
|
||||||
port=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 = {
|
restore_names = {
|
||||||
'postgresql_databases': ['foo', 'bar'],
|
'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(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
flexmock()
|
flexmock()
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||||
flexmock(module).should_receive('get_configured_database').with_args(
|
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||||
config=object,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='foo',
|
data_source_name='foo',
|
||||||
).and_return(('postgresql_databases', {'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,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
).and_return((None, None))
|
).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,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
configuration_database_name='all',
|
configuration_data_source_name='all',
|
||||||
).and_return((None, None))
|
).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,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -420,10 +426,10 @@ def test_run_restore_skips_missing_database():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'foo', 'schemas': None},
|
data_source={'name': 'foo', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('restore_single_database').with_args(
|
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||||
repository=object,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -432,10 +438,10 @@ def test_run_restore_skips_missing_database():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'bar', 'schemas': None},
|
data_source={'name': 'bar', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).never()
|
).never()
|
||||||
flexmock(module).should_receive('ensure_databases_found')
|
flexmock(module).should_receive('ensure_data_sources_found')
|
||||||
|
|
||||||
module.run_restore(
|
module.run_restore(
|
||||||
repository={'path': 'repo'},
|
repository={'path': 'repo'},
|
||||||
@@ -444,7 +450,7 @@ def test_run_restore_skips_missing_database():
|
|||||||
restore_arguments=flexmock(
|
restore_arguments=flexmock(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
archive='archive',
|
archive='archive',
|
||||||
databases=flexmock(),
|
data_sources=flexmock(),
|
||||||
schemas=None,
|
schemas=None,
|
||||||
hostname=None,
|
hostname=None,
|
||||||
port=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 = {
|
restore_names = {
|
||||||
'postgresql_databases': ['foo'],
|
'postgresql_databases': ['foo'],
|
||||||
'mysql_databases': ['bar'],
|
'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(module.borgmatic.borg.rlist).should_receive('resolve_archive_name').and_return(
|
||||||
flexmock()
|
flexmock()
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('collect_archive_database_names').and_return(flexmock())
|
flexmock(module).should_receive('collect_archive_data_source_names').and_return(flexmock())
|
||||||
flexmock(module).should_receive('find_databases_to_restore').and_return(restore_names)
|
flexmock(module).should_receive('find_data_sources_to_restore').and_return(restore_names)
|
||||||
flexmock(module).should_receive('get_configured_database').with_args(
|
flexmock(module).should_receive('get_configured_data_source').with_args(
|
||||||
config=object,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database_name='foo',
|
data_source_name='foo',
|
||||||
).and_return(('postgresql_databases', {'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,
|
config=object,
|
||||||
archive_database_names=object,
|
archive_data_source_names=object,
|
||||||
hook_name='mysql_databases',
|
hook_name='mysql_databases',
|
||||||
database_name='bar',
|
data_source_name='bar',
|
||||||
).and_return(('mysql_databases', {'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,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -492,10 +498,10 @@ def test_run_restore_restores_databases_from_different_hooks():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='postgresql_databases',
|
hook_name='postgresql_databases',
|
||||||
database={'name': 'foo', 'schemas': None},
|
data_source={'name': 'foo', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('restore_single_database').with_args(
|
flexmock(module).should_receive('restore_single_data_source').with_args(
|
||||||
repository=object,
|
repository=object,
|
||||||
config=object,
|
config=object,
|
||||||
local_borg_version=object,
|
local_borg_version=object,
|
||||||
@@ -504,10 +510,10 @@ def test_run_restore_restores_databases_from_different_hooks():
|
|||||||
remote_path=object,
|
remote_path=object,
|
||||||
archive_name=object,
|
archive_name=object,
|
||||||
hook_name='mysql_databases',
|
hook_name='mysql_databases',
|
||||||
database={'name': 'bar', 'schemas': None},
|
data_source={'name': 'bar', 'schemas': None},
|
||||||
connection_params=object,
|
connection_params=object,
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('ensure_databases_found')
|
flexmock(module).should_receive('ensure_data_sources_found')
|
||||||
|
|
||||||
module.run_restore(
|
module.run_restore(
|
||||||
repository={'path': 'repo'},
|
repository={'path': 'repo'},
|
||||||
@@ -516,7 +522,7 @@ def test_run_restore_restores_databases_from_different_hooks():
|
|||||||
restore_arguments=flexmock(
|
restore_arguments=flexmock(
|
||||||
repository='repo',
|
repository='repo',
|
||||||
archive='archive',
|
archive='archive',
|
||||||
databases=flexmock(),
|
data_sources=flexmock(),
|
||||||
schemas=None,
|
schemas=None,
|
||||||
hostname=None,
|
hostname=None,
|
||||||
port=None,
|
port=None,
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'match_archives,archive_name_format,feature_available,expected_result',
|
'match_archives,archive_name_format,feature_available,expected_result',
|
||||||
(
|
(
|
||||||
(None, None, True, ()),
|
(None, None, True, ('--match-archives', 'sh:{hostname}-*')), # noqa: FS003
|
||||||
(None, '', True, ()),
|
(None, '', True, ('--match-archives', 'sh:{hostname}-*')), # noqa: FS003
|
||||||
(
|
(
|
||||||
're:foo-.*',
|
're:foo-.*',
|
||||||
'{hostname}-{now}', # noqa: FS003
|
'{hostname}-{now}', # noqa: FS003
|
||||||
@@ -145,7 +145,12 @@ def test_make_repository_archive_flags_with_borg_features_joins_repository_and_a
|
|||||||
True,
|
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(
|
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():
|
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'))
|
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():
|
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)
|
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||||
|
|
||||||
logs = tuple(
|
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():
|
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)
|
flexmock(module.logger).should_receive('getEffectiveLevel').and_return(logging.WARNING)
|
||||||
|
|
||||||
logs = tuple(
|
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))
|
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():
|
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()))
|
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():
|
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()))
|
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():
|
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()))
|
logs = tuple(module.log_error_records('Error', KeyError()))
|
||||||
|
|
||||||
|
|||||||
@@ -4,34 +4,36 @@ from flexmock import flexmock
|
|||||||
from borgmatic.hooks import dump as module
|
from borgmatic.hooks import dump as module
|
||||||
|
|
||||||
|
|
||||||
def test_make_database_dump_path_joins_arguments():
|
def test_make_data_source_dump_path_joins_arguments():
|
||||||
assert module.make_database_dump_path('/tmp', 'super_databases') == '/tmp/super_databases'
|
assert module.make_data_source_dump_path('/tmp', 'super_databases') == '/tmp/super_databases'
|
||||||
|
|
||||||
|
|
||||||
def test_make_database_dump_path_defaults_without_source_directory():
|
def test_make_data_source_dump_path_defaults_without_source_directory():
|
||||||
assert module.make_database_dump_path(None, 'super_databases') == '~/.borgmatic/super_databases'
|
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')
|
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
module.make_database_dump_filename('databases', 'test', 'hostname')
|
module.make_data_source_dump_filename('databases', 'test', 'hostname')
|
||||||
== 'databases/hostname/test'
|
== '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')
|
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')
|
flexmock(module.os.path).should_receive('expanduser').and_return('databases')
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
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():
|
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')
|
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('expanduser').and_return('databases/localhost')
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.shutil).should_receive('rmtree').with_args('databases/localhost').once()
|
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('expanduser').and_return('databases/localhost')
|
||||||
flexmock(module.os.path).should_receive('exists').never()
|
flexmock(module.os.path).should_receive('exists').never()
|
||||||
flexmock(module.shutil).should_receive('rmtree').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('expanduser').and_return('databases/localhost')
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.shutil).should_receive('rmtree').never()
|
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():
|
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')
|
assert names == ('foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
def test_dump_databases_dumps_each_database():
|
def test_dump_data_sources_dumps_each_database():
|
||||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
@@ -63,10 +63,10 @@ def test_dump_databases_dumps_each_database():
|
|||||||
dry_run_label=object,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}
|
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
@@ -84,10 +84,10 @@ def test_dump_databases_dumps_with_password():
|
|||||||
dry_run_label=object,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all', 'format': 'sql'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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():
|
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():
|
def test_execute_dump_command_runs_mariadb_dump():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mariadb_dump_without_add_drop_database():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mariadb_dump_with_hostname_and_port():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mariadb_dump_with_username_and_password():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mariadb_dump_with_options():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
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.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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():
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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'}]
|
databases = [{'name': 'all'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
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'}]
|
databases = [{'name': 'all'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
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'}]
|
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -391,11 +391,11 @@ def test_restore_database_dump_runs_mariadb_to_restore():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -419,11 +419,11 @@ def test_restore_database_dump_runs_mariadb_with_options():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -456,11 +456,11 @@ def test_restore_database_dump_runs_mariadb_with_hostname_and_port():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
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'},
|
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -533,11 +533,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
extra_environment={'MYSQL_PWD': 'clipassword'},
|
extra_environment={'MYSQL_PWD': 'clipassword'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -584,11 +584,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
extra_environment={'MYSQL_PWD': 'restorepass'},
|
extra_environment={'MYSQL_PWD': 'restorepass'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo'}]
|
||||||
|
|
||||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=True,
|
dry_run=True,
|
||||||
extract_process=flexmock(),
|
extract_process=flexmock(),
|
||||||
connection_params={
|
connection_params={
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ from flexmock import flexmock
|
|||||||
from borgmatic.hooks import mongodb as module
|
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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/foo'
|
||||||
).and_return('databases/localhost/bar')
|
).and_return('databases/localhost/bar')
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/foo'
|
||||||
).and_return('databases/localhost/bar')
|
).and_return('databases/localhost/bar')
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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}]
|
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/database.example.org/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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 = [
|
databases = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -76,7 +76,7 @@ def test_dump_databases_runs_mongodump_with_username_and_password():
|
|||||||
]
|
]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo', 'format': 'directory'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
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,
|
shell=True,
|
||||||
).and_return(flexmock()).once()
|
).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'}]
|
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
['mongorestore', '--archive', '--drop'],
|
['mongorestore', '--archive', '--drop'],
|
||||||
processes=[extract_process],
|
processes=[extract_process],
|
||||||
@@ -177,11 +177,11 @@ def test_restore_database_dump_runs_mongorestore():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
|
{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
|
||||||
]
|
]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
[
|
[
|
||||||
'mongorestore',
|
'mongorestore',
|
||||||
@@ -216,11 +216,11 @@ def test_restore_database_dump_runs_mongorestore_with_hostname_and_port():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -245,7 +245,7 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
|||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
[
|
[
|
||||||
'mongorestore',
|
'mongorestore',
|
||||||
@@ -263,11 +263,11 @@ def test_restore_database_dump_runs_mongorestore_with_username_and_password():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -296,7 +296,7 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
[
|
[
|
||||||
'mongorestore',
|
'mongorestore',
|
||||||
@@ -318,11 +318,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -351,7 +351,7 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
[
|
[
|
||||||
'mongorestore',
|
'mongorestore',
|
||||||
@@ -373,11 +373,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'restore_options': '--harder', 'schemas': None}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
['mongorestore', '--archive', '--drop', '--harder'],
|
['mongorestore', '--archive', '--drop', '--harder'],
|
||||||
processes=[extract_process],
|
processes=[extract_process],
|
||||||
@@ -402,11 +402,11 @@ def test_restore_database_dump_runs_mongorestore_with_options():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
connection_params={
|
||||||
@@ -423,7 +423,7 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
|||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
[
|
[
|
||||||
'mongorestore',
|
'mongorestore',
|
||||||
@@ -439,11 +439,11 @@ def test_restore_databases_dump_runs_mongorestore_with_schemas():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'all', 'schemas': None}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
['mongorestore', '--archive'],
|
['mongorestore', '--archive'],
|
||||||
processes=[extract_process],
|
processes=[extract_process],
|
||||||
@@ -468,11 +468,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
|||||||
input_file=extract_process.stdout,
|
input_file=extract_process.stdout,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'schemas': None}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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()
|
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=True,
|
dry_run=True,
|
||||||
extract_process=flexmock(),
|
extract_process=flexmock(),
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'format': 'directory', 'schemas': None}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
['mongorestore', '--dir', '/dump/path', '--drop'],
|
['mongorestore', '--dir', '/dump/path', '--drop'],
|
||||||
processes=[],
|
processes=[],
|
||||||
@@ -519,11 +519,11 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
|||||||
input_file=None,
|
input_file=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=None,
|
extract_process=None,
|
||||||
connection_params={
|
connection_params={
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def test_database_names_to_dump_queries_mysql_for_database_names():
|
|||||||
assert names == ('foo', 'bar')
|
assert names == ('foo', 'bar')
|
||||||
|
|
||||||
|
|
||||||
def test_dump_databases_dumps_each_database():
|
def test_dump_data_sources_dumps_each_database():
|
||||||
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
@@ -63,10 +63,10 @@ def test_dump_databases_dumps_each_database():
|
|||||||
dry_run_label=object,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}
|
database = {'name': 'foo', 'username': 'root', 'password': 'trustsome1'}
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
@@ -84,10 +84,10 @@ def test_dump_databases_dumps_with_password():
|
|||||||
dry_run_label=object,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all', 'format': 'sql'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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,
|
dry_run_label=object,
|
||||||
).and_return(process).once()
|
).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():
|
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():
|
def test_execute_dump_command_runs_mysqldump():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mysqldump_without_add_drop_database():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mysqldump_with_hostname_and_port():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mysqldump_with_username_and_password():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
def test_execute_dump_command_runs_mysqldump_with_options():
|
||||||
process = flexmock()
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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():
|
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.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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():
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump')
|
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'}]
|
databases = [{'name': 'all'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
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'}]
|
databases = [{'name': 'all'}]
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
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'}]
|
hook_config = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -391,11 +391,11 @@ def test_restore_database_dump_runs_mysql_to_restore():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo', 'restore_options': '--harder'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -419,11 +419,11 @@ def test_restore_database_dump_runs_mysql_with_options():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -456,11 +456,11 @@ def test_restore_database_dump_runs_mysql_with_hostname_and_port():
|
|||||||
extra_environment=None,
|
extra_environment=None,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
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'},
|
extra_environment={'MYSQL_PWD': 'trustsome1'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -533,11 +533,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
extra_environment={'MYSQL_PWD': 'clipassword'},
|
extra_environment={'MYSQL_PWD': 'clipassword'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -584,11 +584,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
extra_environment={'MYSQL_PWD': 'restorepass'},
|
extra_environment={'MYSQL_PWD': 'restorepass'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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'}]
|
hook_config = [{'name': 'foo'}]
|
||||||
|
|
||||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=True,
|
dry_run=True,
|
||||||
extract_process=flexmock(),
|
extract_process=flexmock(),
|
||||||
connection_params={
|
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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
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(
|
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||||
('bar',)
|
('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'
|
'databases/localhost/foo'
|
||||||
).and_return('databases/localhost/bar')
|
).and_return('databases/localhost/bar')
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
flexmock(module).should_receive('database_names_to_dump').and_return(())
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||||
('bar',)
|
('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'
|
'databases/localhost/foo'
|
||||||
).and_return('databases/localhost/bar')
|
).and_return('databases/localhost/bar')
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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'}]
|
databases = [{'name': 'foo'}, {'name': 'bar'}]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',)).and_return(
|
||||||
('bar',)
|
('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'
|
'databases/localhost/foo'
|
||||||
).and_return('databases/localhost/bar')
|
).and_return('databases/localhost/bar')
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
flexmock(module.dump).should_receive('create_named_pipe_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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}]
|
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
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'
|
'databases/database.example.org/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return(
|
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('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo', 'format': 'directory'}]
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).and_return(flexmock()).once()
|
).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'}]
|
databases = [{'name': 'foo', 'options': '--stuff=such'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'all'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('all',))
|
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'
|
'databases/localhost/all'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
databases = [{'name': 'foo', 'pg_dump_command': 'special_pg_dump'}]
|
||||||
process = flexmock()
|
process = flexmock()
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('')
|
flexmock(module).should_receive('make_dump_path').and_return('')
|
||||||
flexmock(module).should_receive('database_names_to_dump').and_return(('foo',))
|
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'
|
'databases/localhost/foo'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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,
|
run_to_completion=False,
|
||||||
).and_return(process).once()
|
).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'}]
|
hook_config = [{'name': 'foo', 'schemas': None}, {'name': 'bar'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -499,11 +499,11 @@ def test_restore_database_dump_runs_pg_restore():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433, 'schemas': None}
|
{'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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -561,11 +561,11 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1', 'schemas': None}
|
{'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'}
|
{'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -621,11 +621,11 @@ def test_restore_database_dump_runs_pg_restore_with_username_and_password():
|
|||||||
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
|
extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -658,7 +658,7 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
|
{'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'}
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -700,11 +700,11 @@ def test_restore_database_dump_with_connection_params_uses_connection_params_for
|
|||||||
extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
|
extra_environment={'PGPASSWORD': 'clipassword', 'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'name': 'foo',
|
||||||
@@ -737,7 +737,7 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
|
{'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'}
|
||||||
)
|
)
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -779,11 +779,11 @@ def test_restore_database_dump_without_connection_params_uses_restore_params_in_
|
|||||||
extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
|
extra_environment={'PGPASSWORD': 'restorepassword', 'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -840,11 +840,11 @@ def test_restore_database_dump_runs_pg_restore_with_options():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'all', 'schemas': None}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'psql',
|
'psql',
|
||||||
@@ -879,11 +879,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'all'},
|
data_source={'name': 'all'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'format': 'plain', 'schemas': None}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
('psql', '--no-password', '--no-psqlrc', '--dbname', 'foo'),
|
('psql', '--no-password', '--no-psqlrc', '--dbname', 'foo'),
|
||||||
processes=[extract_process],
|
processes=[extract_process],
|
||||||
@@ -923,11 +923,11 @@ def test_restore_database_dump_runs_psql_for_plain_database_dump():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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 = [
|
hook_config = [
|
||||||
{
|
{
|
||||||
'name': 'foo',
|
'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_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'docker',
|
'docker',
|
||||||
@@ -988,11 +988,11 @@ def test_restore_database_dump_runs_non_default_pg_restore_and_psql():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'schemas': None}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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()
|
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=True,
|
dry_run=True,
|
||||||
extract_process=flexmock(),
|
extract_process=flexmock(),
|
||||||
connection_params={
|
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}]
|
hook_config = [{'name': 'foo', 'schemas': None}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -1064,11 +1064,11 @@ def test_restore_database_dump_without_extract_process_restores_from_disk():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'foo'},
|
data_source={'name': 'foo'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=None,
|
extract_process=None,
|
||||||
connection_params={
|
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']}]
|
hook_config = [{'name': 'foo', 'schemas': ['bar', 'baz']}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
flexmock(module).should_receive('make_extra_environment').and_return({'PGSSLMODE': 'disable'})
|
||||||
flexmock(module).should_receive('make_dump_path')
|
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(
|
flexmock(module).should_receive('execute_command_with_processes').with_args(
|
||||||
(
|
(
|
||||||
'pg_restore',
|
'pg_restore',
|
||||||
@@ -1120,11 +1120,11 @@ def test_restore_database_dump_with_schemas_restores_schemas():
|
|||||||
extra_environment={'PGSSLMODE': 'disable'},
|
extra_environment={'PGSSLMODE': 'disable'},
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=None,
|
extract_process=None,
|
||||||
connection_params={
|
connection_params={
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ from flexmock import flexmock
|
|||||||
from borgmatic.hooks import sqlite as module
|
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'}]
|
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
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'
|
'/path/to/dump/database'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(True)
|
flexmock(module.os.path).should_receive('exists').and_return(True)
|
||||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
|
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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 = [
|
databases = [
|
||||||
{'path': '/path/to/database1', 'name': 'database1'},
|
{'path': '/path/to/database1', 'name': 'database1'},
|
||||||
{'path': '/path/to/database2', 'name': 'database2'},
|
{'path': '/path/to/database2', 'name': 'database2'},
|
||||||
@@ -27,7 +27,7 @@ def test_dump_databases_dumps_each_database():
|
|||||||
processes = [flexmock(), flexmock()]
|
processes = [flexmock(), flexmock()]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
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'
|
'/path/to/dump/database'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
@@ -36,7 +36,7 @@ def test_dump_databases_dumps_each_database():
|
|||||||
processes[1]
|
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():
|
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).should_receive('make_dump_path').and_return('/path/to/dump')
|
||||||
flexmock(module.logger).should_receive('warning').once()
|
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'
|
'/path/to/dump/database'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
||||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
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():
|
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(
|
flexmock(module.logger).should_receive(
|
||||||
'warning'
|
'warning'
|
||||||
).twice() # once for the name=all, once for the non-existent path
|
).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'
|
'/path/to/dump/database'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
flexmock(module.dump).should_receive('create_parent_directory_for_dump')
|
||||||
flexmock(module).should_receive('execute_command').and_return(processes[0])
|
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'}]
|
databases = [{'path': '/path/to/database', 'name': 'database'}]
|
||||||
|
|
||||||
flexmock(module).should_receive('make_dump_path').and_return('/path/to/dump')
|
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'
|
'/path/to/dump/database'
|
||||||
)
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
flexmock(module.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
|
flexmock(module.dump).should_receive('create_parent_directory_for_dump').never()
|
||||||
flexmock(module).should_receive('execute_command').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'}]
|
hook_config = [{'path': '/path/to/database', 'name': 'database'}, {'name': 'other'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
@@ -107,18 +107,18 @@ def test_restore_database_dump_restores_database():
|
|||||||
|
|
||||||
flexmock(module.os).should_receive('remove').once()
|
flexmock(module.os).should_receive('remove').once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={'restore_path': None},
|
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 = [
|
hook_config = [
|
||||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
{'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()
|
flexmock(module.os).should_receive('remove').once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'database'},
|
data_source={'name': 'database'},
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={'restore_path': 'cli/path/to/database'},
|
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 = [
|
hook_config = [
|
||||||
{'path': '/path/to/database', 'name': 'database', 'restore_path': 'config/path/to/database'}
|
{'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()
|
flexmock(module.os).should_receive('remove').once()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database=hook_config[0],
|
data_source=hook_config[0],
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={'restore_path': None},
|
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'}]
|
hook_config = [{'path': '/path/to/database', 'name': 'database'}]
|
||||||
extract_process = flexmock(stdout=flexmock())
|
extract_process = flexmock(stdout=flexmock())
|
||||||
|
|
||||||
flexmock(module).should_receive('execute_command_with_processes').never()
|
flexmock(module).should_receive('execute_command_with_processes').never()
|
||||||
flexmock(module.os).should_receive('remove').never()
|
flexmock(module.os).should_receive('remove').never()
|
||||||
|
|
||||||
module.restore_database_dump(
|
module.restore_data_source_dump(
|
||||||
hook_config,
|
hook_config,
|
||||||
{},
|
{},
|
||||||
'test.yaml',
|
'test.yaml',
|
||||||
database={'name': 'database'},
|
data_source={'name': 'database'},
|
||||||
dry_run=True,
|
dry_run=True,
|
||||||
extract_process=extract_process,
|
extract_process=extract_process,
|
||||||
connection_params={'restore_path': None},
|
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)
|
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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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('Console_color_formatter')
|
||||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
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)
|
flexmock(module.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||||
syslog_handler = logging.handlers.SysLogHandler()
|
syslog_handler = logging.handlers.SysLogHandler()
|
||||||
@@ -191,19 +193,21 @@ def test_configure_logging_probes_for_log_socket_on_linux():
|
|||||||
address='/dev/log'
|
address='/dev/log'
|
||||||
).and_return(syslog_handler).once()
|
).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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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('Console_color_formatter')
|
||||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
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('/dev/log').and_return(False)
|
||||||
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').and_return(True)
|
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'
|
address='/var/run/syslog'
|
||||||
).and_return(syslog_handler).once()
|
).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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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('Console_color_formatter')
|
||||||
flexmock(module).should_receive('interactive_console').and_return(False)
|
flexmock(module).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
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('/dev/log').and_return(False)
|
||||||
flexmock(module.os.path).should_receive('exists').with_args('/var/run/syslog').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'
|
address='/var/run/log'
|
||||||
).and_return(syslog_handler).once()
|
).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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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('Console_color_formatter')
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
flexmock(module.logging).should_receive('basicConfig').with_args(
|
||||||
level=logging.DEBUG, handlers=tuple
|
level=logging.INFO, handlers=list
|
||||||
).once()
|
)
|
||||||
flexmock(module.os.path).should_receive('exists').and_return(False)
|
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():
|
def test_configure_logging_skips_syslog_if_not_found():
|
||||||
flexmock(module).should_receive('add_custom_log_levels')
|
flexmock(module).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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('Console_color_formatter')
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
||||||
|
|
||||||
module.configure_logging(console_log_level=logging.INFO)
|
module.configure_logging(console_log_level=logging.INFO, syslog_log_level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
|
def test_configure_logging_skips_log_file_if_log_file_logging_is_disabled():
|
||||||
flexmock(module).should_receive('add_custom_log_levels')
|
flexmock(module).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).DISABLED = module.DISABLED
|
flexmock(module.logging).DISABLED = module.DISABLED
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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(
|
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.os.path).should_receive('exists').never()
|
||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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(
|
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.os.path).should_receive('exists').never()
|
||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').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()
|
).and_return(file_handler).once()
|
||||||
|
|
||||||
module.configure_logging(
|
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
|
'{message}', style='{' # noqa: FS003
|
||||||
).once()
|
).once()
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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).should_receive('interactive_console').and_return(False)
|
||||||
flexmock(module.logging).should_receive('basicConfig').with_args(
|
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.os.path).should_receive('exists').with_args('/dev/log').and_return(True)
|
||||||
flexmock(module.logging.handlers).should_receive('SysLogHandler').never()
|
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).should_receive('add_custom_log_levels')
|
||||||
flexmock(module.logging).ANSWER = module.ANSWER
|
flexmock(module.logging).ANSWER = module.ANSWER
|
||||||
flexmock(module).should_receive('Multi_stream_handler').and_return(
|
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(
|
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.os.path).should_receive('exists').and_return(False)
|
||||||
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').never()
|
flexmock(module.logging.handlers).should_receive('WatchedFileHandler').never()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
from borgmatic import signals as module
|
from borgmatic import signals as module
|
||||||
@@ -34,6 +35,17 @@ def test_handle_signal_exits_on_sigterm():
|
|||||||
module.handle_signal(signal_number, frame)
|
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():
|
def test_configure_signals_installs_signal_handlers():
|
||||||
flexmock(module.signal).should_receive('signal').at_least().once()
|
flexmock(module.signal).should_receive('signal').at_least().once()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user