Compare commits

..
24 Commits
Author SHA1 Message Date
Dan Helfman cacb81f086 Bump version for release. 2023-09-30 13:37:10 -07:00
Dan Helfman 06c2154e6a Build docs regardless of Drone "event" (push, etc.). 2023-09-29 19:49:09 -07:00
Dan Helfman ac1e1a9407 Simplify logging logic (#665). 2023-09-29 14:16:47 -07:00
Dan Helfman 10933fd55b Fix for borgmatic not stopping Borg immediately when the user presses ctrl-C (#761). 2023-09-27 08:52:00 -07:00
Dan Helfman af422ad705 Add documentation note about upgrading multiple pipx installations of borgmatic. 2023-09-18 13:46:41 -07:00
Dan Helfman d9d35491fb Fix tense typo. 2023-09-17 23:25:57 -07:00
Dan Helfman b540e63c0e Updated documentation so "sudo borgmatic" works for pipx borgmatic installations (#757). 2023-09-17 22:46:33 -07:00
Dan Helfman 5a56208922 Fix documentation typo. 2023-09-15 10:18:35 -07:00
Dan Helfman 5912769273 Fix error handling to log command output as one record per line (#754). 2023-09-14 21:10:52 -07:00
Dan Helfman bac2aabe66 Attempt to unbreak ticket filing. 2023-09-12 09:50:38 -07:00
Dan Helfman 9f3328781b When "archive_name_format" is not set, filter archives using the default archive name format (#753). 2023-09-06 23:13:40 -07:00
Dan Helfman 0205748db8 Update documentation to recommend installing/upgrading borgmatic with pipx instead of pip. 2023-09-04 16:25:10 -07:00
Dan Helfman d0a8251ad2 Add borgmatic version introducing Loki hook to docs (#743). 2023-08-27 20:30:13 -07:00
Dan Helfman 32019ea8f3 Add documentation for Grafana Loki hook (#743). 2023-08-25 10:52:00 -07:00
Dan Helfman fa9a061033 Merge branch 'main' of ssh://projects.torsion.org:3022/borgmatic-collective/borgmatic 2023-08-25 09:29:02 -07:00
Dan Helfman b3d2560563 Added support for grafana loki (#743).
Reviewed-on: https://projects.torsion.org/borgmatic-collective/borgmatic/pulls/747
2023-08-25 16:28:19 +00:00
Dan Helfman 4b4f56da42 Fix another database -> data source instance (#685). 2023-08-24 14:00:29 -07:00
Dan Helfman b96d1898f7 Prep work for eventual container-dumping hooks: Generalize internal database hook "API" (#685). 2023-08-24 13:50:10 -07:00
Tobias Hodapp 099a712e53 Added more documentation to the test
Split tests to integration tests
2023-08-24 13:17:42 +02:00
Tobias Hodapp 9e2674ea5a Added unit tests
Removed useless dry run check
2023-08-23 17:17:23 +02:00
Tobias Hodapp 7e419ec995 Fixed spelling errors
Added documentation
Added log messages for dry run
2023-08-22 23:03:14 +02:00
Tobias Hodapp a3edf757ee Added changes of formatting tools 2023-08-22 13:40:05 +02:00
Tobias Hodapp e576403b64 Added support for grafana loki 2023-08-22 03:13:39 +02:00
Dan Helfman 7313430178 Make warning about sections a little more explicit (#721). 2023-08-19 22:51:20 -07:00
44 changed files with 1474 additions and 953 deletions
-2
View File
@@ -93,5 +93,3 @@ trigger:
- borgmatic-collective/borgmatic - borgmatic-collective/borgmatic
branch: branch:
- main - main
event:
- push
+1 -1
View File
@@ -1 +1 @@
blank_issues_enabled: false blank_issues_enabled: true
+24 -4
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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)
+1 -4
View File
@@ -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):
+9 -7
View File
@@ -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 ()
+3 -3
View File
@@ -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
+14 -13
View File
@@ -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',
+12 -11
View File
@@ -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)
+1 -1
View File
@@ -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.',
) )
) )
] ]
+30
View File
@@ -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.
+1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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)
+154
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
+46 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+18 -18
View File
@@ -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
+1 -1
View File
@@ -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:
+90 -43
View File
@@ -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
+37 -42
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -7
View File
@@ -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():
+89
View File
@@ -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
View File
@@ -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,
+8 -3
View File
@@ -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(
+20 -6
View File
@@ -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()))
+18 -16
View File
@@ -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():
+108
View File
@@ -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()
+42 -42
View File
@@ -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={
+60 -60
View File
@@ -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={
+42 -42
View File
@@ -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={
+79 -79
View File
@@ -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={
+25 -25
View File
@@ -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
View File
@@ -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()
+12
View File
@@ -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()