Fix the "restore" action to work on database dumps without a port when a default port is configured (#969).

This commit is contained in:
Dan Helfman
2025-01-17 13:46:18 -08:00
parent f183800009
commit f1cac95b9c
8 changed files with 113 additions and 11 deletions
+2
View File
@@ -12,6 +12,8 @@
* #962: For the LVM hook, add support for nested logical volumes.
* #965: Fix a borgmatic runtime directory error when running the "spot" check with a database hook
enabled.
* #969: Fix the "restore" action to work on database dumps without a port when a default port is
present in configuration.
* Fix the "spot" check to no longer consider pipe files within an archive for file comparisons.
* Fix the "spot" check to have a nicer error when there are no source paths to compare.
* Fix auto-excluding of special files (when databases are configured) to support relative source
+24 -6
View File
@@ -27,15 +27,22 @@ Dump = collections.namedtuple(
)
def dumps_match(first, second):
def dumps_match(first, second, default_port=None):
'''
Compare two Dump instances for equality while supporting a field value of UNSPECIFIED, which
indicates that the field should match any value.
indicates that the field should match any value. If a default port is given, then consider any
dump having that port to match with a dump having a None port.
'''
for field_name in first._fields:
first_value = getattr(first, field_name)
second_value = getattr(second, field_name)
if default_port is not None and field_name == 'port':
if first_value == default_port and second_value is None:
continue
if second_value == default_port and first_value is None:
continue
if first_value == UNSPECIFIED or second_value == UNSPECIFIED:
continue
@@ -64,10 +71,10 @@ def render_dump_metadata(dump):
return metadata
def get_configured_data_source(config, restore_dump):
def get_configured_data_source(config, restore_dump, log_prefix):
'''
Search in the given configuration dict for dumps corresponding to the given dump to restore. If
there are multiple matches, error.
there are multiple matches, error. Log using the given log prefix.
Return the found data source as a data source configuration dict or None if not found.
'''
@@ -78,8 +85,16 @@ def get_configured_data_source(config, restore_dump):
matching_dumps = tuple(
hook_data_source
for (hook_name, hook) in hooks_to_search.items()
for hook_data_source in hook
for (hook_name, hook_config) in hooks_to_search.items()
for hook_data_source in hook_config
for default_port in (
borgmatic.hooks.dispatch.call_hook(
function_name='get_default_port',
config=config,
log_prefix=log_prefix,
hook_name=hook_name,
),
)
if dumps_match(
Dump(
hook_name,
@@ -88,6 +103,7 @@ def get_configured_data_source(config, restore_dump):
hook_data_source.get('port'),
),
restore_dump,
default_port,
)
)
@@ -478,6 +494,7 @@ def run_restore(
found_data_source = get_configured_data_source(
config,
restore_dump,
log_prefix=repository['path'],
)
# For a dump that wasn't found via an exact match in the configuration, try to fallback
@@ -486,6 +503,7 @@ def run_restore(
found_data_source = get_configured_data_source(
config,
Dump(restore_dump.hook_name, 'all', restore_dump.hostname, restore_dump.port),
log_prefix=repository['path'],
)
if not found_data_source:
+4
View File
@@ -118,6 +118,10 @@ def execute_dump_command(
)
def get_default_port(databases, config, log_prefix): # pragma: no cover
return 3306
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MariaDB database configuration dicts, a configuration dict (ignored), and a
+4
View File
@@ -17,6 +17,10 @@ def make_dump_path(base_directory): # pragma: no cover
return dump.make_data_source_dump_path(base_directory, 'mongodb_databases')
def get_default_port(databases, config, log_prefix): # pragma: no cover
return 27017
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MongoDB database configuration dicts, a configuration dict (ignored), and a
+4
View File
@@ -117,6 +117,10 @@ def execute_dump_command(
)
def get_default_port(databases, config, log_prefix): # pragma: no cover
return 3306
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of MySQL database configuration dicts, a configuration dict (ignored), and a
@@ -97,6 +97,10 @@ def database_names_to_dump(database, extra_environment, log_prefix, dry_run):
)
def get_default_port(databases, config, log_prefix): # pragma: no cover
return 5432
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of PostgreSQL database configuration dicts, a configuration dict (ignored), and
+4
View File
@@ -17,6 +17,10 @@ def make_dump_path(base_directory): # pragma: no cover
return dump.make_data_source_dump_path(base_directory, 'sqlite_databases')
def get_default_port(databases, config, log_prefix): # pragma: no cover
return None # SQLite doesn't use a port.
def use_streaming(databases, config, log_prefix):
'''
Given a sequence of SQLite database configuration dicts, a configuration dict (ignored), and a
+67 -5
View File
@@ -5,73 +5,96 @@ import borgmatic.actions.restore as module
@pytest.mark.parametrize(
'first_dump,second_dump,expected_result',
'first_dump,second_dump,default_port,expected_result',
(
(
module.Dump('postgresql_databases', 'foo'),
module.Dump('postgresql_databases', 'foo'),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo'),
module.Dump('postgresql_databases', 'bar'),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo'),
module.Dump('mariadb_databases', 'foo'),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo'),
module.Dump(module.UNSPECIFIED, 'foo'),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo'),
module.Dump(module.UNSPECIFIED, 'bar'),
None,
False,
),
(module.Dump('postgresql_databases', 'foo'), module.Dump(module.UNSPECIFIED, 'foo'), True),
(module.Dump('postgresql_databases', 'foo'), module.Dump(module.UNSPECIFIED, 'bar'), False),
(
module.Dump('postgresql_databases', module.UNSPECIFIED),
module.Dump('postgresql_databases', 'foo'),
None,
True,
),
(
module.Dump('postgresql_databases', module.UNSPECIFIED),
module.Dump('mariadb_databases', 'foo'),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost'),
module.Dump('postgresql_databases', 'foo', 'myhost'),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost'),
module.Dump('postgresql_databases', 'foo', 'otherhost'),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost'),
module.Dump('postgresql_databases', 'foo', module.UNSPECIFIED),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost'),
module.Dump('postgresql_databases', 'bar', module.UNSPECIFIED),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', 1234),
module.Dump('postgresql_databases', 'foo', 'myhost', 1234),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', 1234),
module.Dump('postgresql_databases', 'foo', 'myhost', 4321),
None,
False,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', module.UNSPECIFIED),
module.Dump('postgresql_databases', 'foo', 'myhost', 1234),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', module.UNSPECIFIED),
module.Dump('postgresql_databases', 'foo', 'otherhost', 1234),
None,
False,
),
(
@@ -79,14 +102,33 @@ import borgmatic.actions.restore as module
module.UNSPECIFIED, module.UNSPECIFIED, module.UNSPECIFIED, module.UNSPECIFIED
),
module.Dump('postgresql_databases', 'foo', 'myhost', 1234),
None,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', 5432),
module.Dump('postgresql_databases', 'foo', 'myhost', None),
5432,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', None),
module.Dump('postgresql_databases', 'foo', 'myhost', 5432),
5432,
True,
),
(
module.Dump('postgresql_databases', 'foo', 'myhost', 5433),
module.Dump('postgresql_databases', 'foo', 'myhost', None),
5432,
False,
),
),
)
def test_dumps_match_compares_two_dumps_while_respecting_unspecified_values(
first_dump, second_dump, expected_result
first_dump, second_dump, default_port, expected_result
):
assert module.dumps_match(first_dump, second_dump) == expected_result
assert module.dumps_match(first_dump, second_dump, default_port) == expected_result
@pytest.mark.parametrize(
@@ -137,10 +179,13 @@ def test_render_dump_metadata_renders_dump_values_into_string(dump, expected_res
def test_get_configured_data_source_matches_data_source_with_restore_dump():
default_port = flexmock()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').and_return(default_port)
flexmock(module).should_receive('dumps_match').and_return(False)
flexmock(module).should_receive('dumps_match').with_args(
module.Dump('postgresql_databases', 'bar'),
module.Dump('postgresql_databases', 'bar'),
default_port=default_port,
).and_return(True)
assert module.get_configured_data_source(
@@ -149,22 +194,26 @@ def test_get_configured_data_source_matches_data_source_with_restore_dump():
'postgresql_databases': [{'name': 'foo'}, {'name': 'bar'}],
},
restore_dump=module.Dump('postgresql_databases', 'bar'),
log_prefix='test',
) == {'name': 'bar'}
def test_get_configured_data_source_matches_nothing_when_nothing_configured():
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').and_return(flexmock())
flexmock(module).should_receive('dumps_match').and_return(False)
assert (
module.get_configured_data_source(
config={},
restore_dump=module.Dump('postgresql_databases', 'quux'),
log_prefix='test',
)
is None
)
def test_get_configured_data_source_matches_nothing_when_restore_dump_does_not_match_configuration():
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').and_return(flexmock())
flexmock(module).should_receive('dumps_match').and_return(False)
assert (
@@ -173,16 +222,20 @@ def test_get_configured_data_source_matches_nothing_when_restore_dump_does_not_m
'postgresql_databases': [{'name': 'foo'}],
},
restore_dump=module.Dump('postgresql_databases', 'quux'),
log_prefix='test',
)
is None
)
def test_get_configured_data_source_with_multiple_matching_data_sources_errors():
default_port = flexmock()
flexmock(module.borgmatic.hooks.dispatch).should_receive('call_hook').and_return(default_port)
flexmock(module).should_receive('dumps_match').and_return(False)
flexmock(module).should_receive('dumps_match').with_args(
module.Dump('postgresql_databases', 'bar'),
module.Dump('postgresql_databases', 'bar'),
default_port=default_port,
).and_return(True)
flexmock(module).should_receive('render_dump_metadata').and_return('test')
@@ -197,6 +250,7 @@ def test_get_configured_data_source_with_multiple_matching_data_sources_errors()
],
},
restore_dump=module.Dump('postgresql_databases', 'bar'),
log_prefix='test',
)
@@ -1010,14 +1064,17 @@ def test_run_restore_restores_data_source_configured_with_all_name():
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='foo'),
log_prefix=object,
).and_return({'name': 'foo'})
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='bar'),
log_prefix=object,
).and_return(None)
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='all'),
log_prefix=object,
).and_return({'name': 'bar'})
flexmock(module).should_receive('restore_single_dump').with_args(
repository=object,
@@ -1091,14 +1148,17 @@ def test_run_restore_skips_missing_data_source():
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='foo'),
log_prefix=object,
).and_return({'name': 'foo'})
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='bar'),
log_prefix=object,
).and_return(None)
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='all'),
log_prefix=object,
).and_return(None)
flexmock(module).should_receive('restore_single_dump').with_args(
repository=object,
@@ -1172,10 +1232,12 @@ def test_run_restore_restores_data_sources_from_different_hooks():
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='postgresql_databases', data_source_name='foo'),
log_prefix=object,
).and_return({'name': 'foo'})
flexmock(module).should_receive('get_configured_data_source').with_args(
config=object,
restore_dump=module.Dump(hook_name='mysql_databases', data_source_name='foo'),
log_prefix=object,
).and_return({'name': 'bar'})
flexmock(module).should_receive('restore_single_dump').with_args(
repository=object,