Fix for "borgmatic restore" showing success and incorrectly extracting archive files, even when no databases are configured to restore (#246).

This commit is contained in:
Dan Helfman 2019-11-13 10:41:57 -08:00
parent 612e1fea67
commit 6cdc92bd0c
6 changed files with 66 additions and 4 deletions

View file

@ -11,4 +11,4 @@ Robin `ypid` Schneider: Support additional options of Borg and add validate-borg
Scott Squires: Custom archive names Scott Squires: Custom archive names
Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option Thomas LÉVEIL: Support for a keep_minutely prune option. Support for the --json option
Any many others! See the output of "git log". And many others! See the output of "git log".

5
NEWS
View file

@ -1,4 +1,7 @@
1.4.10.dev0 1.4.10
* #246: Fix for "borgmatic restore" showing success and incorrectly extracting archive files, even
when no databases are configured to restore. As this can overwrite files from the archive and
lead to data loss, please upgrade to get the fix before using "borgmatic restore".
* Reopen the file given by "--log-file" flag if an external program rotates the log file while * Reopen the file given by "--log-file" flag if an external program rotates the log file while
borgmatic is running. borgmatic is running.

View file

@ -266,12 +266,13 @@ def run_actions(
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
restore_names, restore_names,
) )
borg_extract.extract_archive( borg_extract.extract_archive(
global_arguments.dry_run, global_arguments.dry_run,
repository, repository,
arguments['restore'].archive, arguments['restore'].archive,
dump.convert_glob_patterns_to_borg_patterns( dump.convert_glob_patterns_to_borg_patterns(
[pattern for patterns in dump_patterns.values() for pattern in patterns] dump.flatten_dump_patterns(dump_patterns, restore_names)
), ),
location, location,
storage, storage,

View file

@ -20,6 +20,26 @@ def make_database_dump_filename(dump_path, name, hostname=None):
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)
def flatten_dump_patterns(dump_patterns, names):
'''
Given a dict from a database hook name to glob patterns matching the dumps for the named
databases, flatten out all the glob patterns into a single sequence, and return it.
Raise ValueError if there are no resulting glob patterns, which indicates that databases are not
configured in borgmatic's configuration.
'''
flattened = [pattern for patterns in dump_patterns.values() for pattern in patterns]
if not flattened:
raise ValueError(
'Cannot restore database(s) {} missing from borgmatic\'s configuration'.format(
', '.join(names) or '"all"'
)
)
return flattened
def remove_database_dumps(dump_path, databases, database_type_name, log_prefix, dry_run): def remove_database_dumps(dump_path, databases, database_type_name, log_prefix, dry_run):
''' '''
Remove the database dumps for the given databases in the dump directory path. The databases are Remove the database dumps for the given databases in the dump directory path. The databases are
@ -121,6 +141,11 @@ def get_per_hook_database_configurations(hooks, names, dump_patterns):
} }
if not names or 'all' in names: if not names or 'all' in names:
if not any(hook_databases.values()):
raise ValueError(
'Cannot restore database "all", as there are no database dumps in the archive'
)
return hook_databases return hook_databases
found_names = { found_names = {

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.4.10.dev0' VERSION = '1.4.10'
setup( setup(

View file

@ -26,6 +26,27 @@ def test_make_database_dump_filename_with_invalid_name_raises():
module.make_database_dump_filename('databases', 'invalid/name') module.make_database_dump_filename('databases', 'invalid/name')
def test_flatten_dump_patterns_produces_list_of_all_patterns():
dump_patterns = {'postgresql_databases': ['*/glob', 'glob/*'], 'mysql_databases': ['*/*/*']}
expected_patterns = dump_patterns['postgresql_databases'] + dump_patterns['mysql_databases']
assert module.flatten_dump_patterns(dump_patterns, ('bob',)) == expected_patterns
def test_flatten_dump_patterns_with_no_patterns_errors():
dump_patterns = {'postgresql_databases': [], 'mysql_databases': []}
with pytest.raises(ValueError):
assert module.flatten_dump_patterns(dump_patterns, ('bob',))
def test_flatten_dump_patterns_with_no_hooks_errors():
dump_patterns = {}
with pytest.raises(ValueError):
assert module.flatten_dump_patterns(dump_patterns, ('bob',))
def test_remove_database_dumps_removes_dump_for_each_database(): def test_remove_database_dumps_removes_dump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_database_dump_filename').and_return( flexmock(module).should_receive('make_database_dump_filename').and_return(
@ -134,3 +155,15 @@ def test_get_per_hook_database_configurations_with_unknown_database_name_raises(
with pytest.raises(ValueError): with pytest.raises(ValueError):
module.get_per_hook_database_configurations(hooks, names, dump_patterns) module.get_per_hook_database_configurations(hooks, names, dump_patterns)
def test_get_per_hook_database_configurations_with_all_and_no_archive_dumps_raises():
hooks = {'postgresql_databases': [flexmock()]}
names = ('foo', 'all')
dump_patterns = flexmock()
flexmock(module).should_receive('get_database_configurations').with_args(
hooks['postgresql_databases'], names
).and_return([])
with pytest.raises(ValueError):
module.get_per_hook_database_configurations(hooks, names, dump_patterns)