Optionally change the internal database dump path via "borgmatic_source_directory" option in location configuration section (#259).

This commit is contained in:
Dan Helfman 2019-12-10 16:04:34 -08:00
parent 826e4352d1
commit 8660af745e
13 changed files with 175 additions and 65 deletions

4
NEWS
View file

@ -1,3 +1,7 @@
1.4.19
* #259: Optionally change the internal database dump path via "borgmatic_source_directory" option
in location configuration section.
1.4.18 1.4.18
* Fix "--repository" flag to accept relative paths. * Fix "--repository" flag to accept relative paths.
* Fix "borgmatic umount" so it only runs Borg once instead of once per repository / configuration * Fix "borgmatic umount" so it only runs Borg once instead of once per repository / configuration

View file

@ -104,16 +104,19 @@ def _make_exclude_flags(location_config, exclude_filename=None):
) )
BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic' DEFAULT_BORGMATIC_SOURCE_DIRECTORY = '~/.borgmatic'
def borgmatic_source_directories(): def 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.
''' '''
if not borgmatic_source_directory:
borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return ( return (
[BORGMATIC_SOURCE_DIRECTORY] [borgmatic_source_directory]
if os.path.exists(os.path.expanduser(BORGMATIC_SOURCE_DIRECTORY)) if os.path.exists(os.path.expanduser(borgmatic_source_directory))
else [] else []
) )
@ -134,7 +137,8 @@ def create_archive(
storage config dict, create a Borg archive and return Borg's JSON output (if any). storage config dict, create a Borg archive and return Borg's JSON output (if any).
''' '''
sources = _expand_directories( sources = _expand_directories(
location_config['source_directories'] + borgmatic_source_directories() location_config['source_directories']
+ borgmatic_source_directories(location_config.get('borgmatic_source_directory'))
) )
pattern_file = _write_pattern_file(location_config.get('patterns')) pattern_file = _write_pattern_file(location_config.get('patterns'))

View file

@ -75,6 +75,7 @@ def run_configuration(config_filename, config, arguments):
hooks, hooks,
config_filename, config_filename,
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
location,
global_arguments.dry_run, global_arguments.dry_run,
) )
except (OSError, CalledProcessError) as error: except (OSError, CalledProcessError) as error:
@ -111,6 +112,7 @@ def run_configuration(config_filename, config, arguments):
hooks, hooks,
config_filename, config_filename,
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
location,
global_arguments.dry_run, global_arguments.dry_run,
) )
command.execute_hook( command.execute_hook(
@ -294,6 +296,7 @@ def run_actions(
hooks, hooks,
repository, repository,
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
location,
restore_names, restore_names,
) )
@ -325,6 +328,7 @@ def run_actions(
restore_databases, restore_databases,
repository, repository,
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
location,
global_arguments.dry_run, global_arguments.dry_run,
) )
dispatch.call_hooks( dispatch.call_hooks(
@ -332,6 +336,7 @@ def run_actions(
restore_databases, restore_databases,
repository, repository,
dump.DATABASE_HOOK_NAMES, dump.DATABASE_HOOK_NAMES,
location,
global_arguments.dry_run, global_arguments.dry_run,
) )
if 'list' in arguments: if 'list' in arguments:

View file

@ -137,6 +137,14 @@ map:
desc: | desc: |
Exclude files with the NODUMP flag. Defaults to false. Exclude files with the NODUMP flag. Defaults to false.
example: true example: true
borgmatic_source_directory:
type: str
desc: |
Path for additional source files used for temporary internal state like
borgmatic database dumps. Note that changing this path prevents "borgmatic
restore" from finding any database dumps created before the change. Defaults
to ~/.borgmatic
example: /tmp/borgmatic
storage: storage:
desc: | desc: |
Repository storage options. See Repository storage options. See

View file

@ -2,11 +2,24 @@ import glob
import logging import logging
import os import os
from borgmatic.borg.create import DEFAULT_BORGMATIC_SOURCE_DIRECTORY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DATABASE_HOOK_NAMES = ('postgresql_databases', 'mysql_databases') DATABASE_HOOK_NAMES = ('postgresql_databases', 'mysql_databases')
def make_database_dump_path(borgmatic_source_directory, database_hook_name):
'''
Given a borgmatic source directory (or None) and a database hook name, construct a database dump
path.
'''
if not borgmatic_source_directory:
borgmatic_source_directory = DEFAULT_BORGMATIC_SOURCE_DIRECTORY
return os.path.join(borgmatic_source_directory, database_hook_name)
def make_database_dump_filename(dump_path, name, hostname=None): def make_database_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, database name, and hostname, return a filename to use

View file

@ -4,15 +4,24 @@ import os
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.hooks import dump from borgmatic.hooks import dump
DUMP_PATH = '~/.borgmatic/mysql_databases'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def dump_databases(databases, log_prefix, dry_run): def make_dump_path(location_config): # pragma: no cover
'''
Make the dump path from the given location configuration and the name of this hook.
'''
return dump.make_database_dump_path(
location_config.get('borgmatic_source_directory'), 'mysql_databases'
)
def dump_databases(databases, log_prefix, location_config, dry_run):
''' '''
Dump the given MySQL/MariaDB databases to disk. The databases are supplied as a sequence of Dump the given MySQL/MariaDB databases to disk. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given log dicts, one dict describing each database as per the configuration schema. Use the given log
prefix in any log entries. If this is a dry run, then don't actually dump anything. prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually dump anything.
''' '''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else '' dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
@ -20,7 +29,9 @@ def dump_databases(databases, log_prefix, dry_run):
for database in databases: for database in databases:
name = database['name'] name = database['name']
dump_filename = dump.make_database_dump_filename(DUMP_PATH, name, database.get('hostname')) dump_filename = dump.make_database_dump_filename(
make_dump_path(location_config), name, database.get('hostname')
)
command = ( command = (
('mysqldump', '--add-drop-database') ('mysqldump', '--add-drop-database')
+ (('--host', database['hostname']) if 'hostname' in database else ()) + (('--host', database['hostname']) if 'hostname' in database else ())
@ -44,37 +55,43 @@ def dump_databases(databases, log_prefix, dry_run):
) )
def remove_database_dumps(databases, log_prefix, dry_run): # pragma: no cover def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
''' '''
Remove the database dumps for the given databases. The databases are supplied as a sequence of Remove the database dumps for the given databases. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the log prefix in dicts, one dict describing each database as per the configuration schema. Use the log prefix in
any log entries. If this is a dry run, then don't actually remove anything. any log entries. Use the given location configuration dict to construct the destination path. If
this is a dry run, then don't actually remove anything.
''' '''
dump.remove_database_dumps(DUMP_PATH, databases, 'MySQL', log_prefix, dry_run) dump.remove_database_dumps(
make_dump_path(location_config), databases, 'MySQL', log_prefix, dry_run
)
def make_database_dump_patterns(databases, log_prefix, names): def make_database_dump_patterns(databases, log_prefix, location_config, names):
''' '''
Given a sequence of configurations dicts, a prefix to log with, and a sequence of database Given a sequence of configurations dicts, a prefix to log with, a location configuration dict,
names to match, return the corresponding glob patterns to match the database dumps in an and a sequence of database names to match, return the corresponding glob patterns to match the
archive. An empty sequence of names indicates that the patterns should match all dumps. database dumps in an archive. An empty sequence of names indicates that the patterns should
match all dumps.
''' '''
return [ return [
dump.make_database_dump_filename(DUMP_PATH, name, hostname='*') for name in (names or ['*']) dump.make_database_dump_filename(make_dump_path(location_config), name, hostname='*')
for name in (names or ['*'])
] ]
def restore_database_dumps(databases, log_prefix, dry_run): def restore_database_dumps(databases, log_prefix, location_config, dry_run):
''' '''
Restore the given MySQL/MariaDB databases from disk. The databases are supplied as a sequence of Restore the given MySQL/MariaDB databases from disk. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given log dicts, one dict describing each database as per the configuration schema. Use the given log
prefix in any log entries. If this is a dry run, then don't actually restore anything. prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually restore anything.
''' '''
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 ''
for database in databases: for database in databases:
dump_filename = dump.make_database_dump_filename( dump_filename = dump.make_database_dump_filename(
DUMP_PATH, database['name'], database.get('hostname') make_dump_path(location_config), database['name'], database.get('hostname')
) )
restore_command = ( restore_command = (
('mysql', '--batch') ('mysql', '--batch')

View file

@ -4,15 +4,24 @@ import os
from borgmatic.execute import execute_command from borgmatic.execute import execute_command
from borgmatic.hooks import dump from borgmatic.hooks import dump
DUMP_PATH = '~/.borgmatic/postgresql_databases'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def dump_databases(databases, log_prefix, dry_run): def make_dump_path(location_config): # pragma: no cover
'''
Make the dump path from the given location configuration and the name of this hook.
'''
return dump.make_database_dump_path(
location_config.get('borgmatic_source_directory'), 'postgresql_databases'
)
def dump_databases(databases, log_prefix, location_config, dry_run):
''' '''
Dump the given PostgreSQL databases to disk. The databases are supplied as a sequence of dicts, Dump the given PostgreSQL databases to disk. The databases are supplied as a sequence of dicts,
one dict describing each database as per the configuration schema. Use the given log prefix in one dict describing each database as per the configuration schema. Use the given log prefix in
any log entries. If this is a dry run, then don't actually dump anything. any log entries. Use the given location configuration dict to construct the destination path. If
this is a dry run, then don't actually dump anything.
''' '''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else '' dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
@ -20,7 +29,9 @@ def dump_databases(databases, log_prefix, dry_run):
for database in databases: for database in databases:
name = database['name'] name = database['name']
dump_filename = dump.make_database_dump_filename(DUMP_PATH, name, database.get('hostname')) dump_filename = dump.make_database_dump_filename(
make_dump_path(location_config), name, database.get('hostname')
)
all_databases = bool(name == 'all') all_databases = bool(name == 'all')
command = ( command = (
('pg_dumpall' if all_databases else 'pg_dump', '--no-password', '--clean') ('pg_dumpall' if all_databases else 'pg_dump', '--no-password', '--clean')
@ -44,37 +55,43 @@ def dump_databases(databases, log_prefix, dry_run):
execute_command(command, extra_environment=extra_environment) execute_command(command, extra_environment=extra_environment)
def remove_database_dumps(databases, log_prefix, dry_run): # pragma: no cover def remove_database_dumps(databases, log_prefix, location_config, dry_run): # pragma: no cover
''' '''
Remove the database dumps for the given databases. The databases are supplied as a sequence of Remove the database dumps for the given databases. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the log prefix in dicts, one dict describing each database as per the configuration schema. Use the log prefix in
any log entries. If this is a dry run, then don't actually remove anything. any log entries. Use the given location configuration dict to construct the destination path. If
this is a dry run, then don't actually remove anything.
''' '''
dump.remove_database_dumps(DUMP_PATH, databases, 'PostgreSQL', log_prefix, dry_run) dump.remove_database_dumps(
make_dump_path(location_config), databases, 'PostgreSQL', log_prefix, dry_run
)
def make_database_dump_patterns(databases, log_prefix, names): def make_database_dump_patterns(databases, log_prefix, location_config, names):
''' '''
Given a sequence of configurations dicts, a prefix to log with, and a sequence of database Given a sequence of configurations dicts, a prefix to log with, a location configuration dict,
names to match, return the corresponding glob patterns to match the database dumps in an and a sequence of database names to match, return the corresponding glob patterns to match the
archive. An empty sequence of names indicates that the patterns should match all dumps. database dumps in an archive. An empty sequence of names indicates that the patterns should
match all dumps.
''' '''
return [ return [
dump.make_database_dump_filename(DUMP_PATH, name, hostname='*') for name in (names or ['*']) dump.make_database_dump_filename(make_dump_path(location_config), name, hostname='*')
for name in (names or ['*'])
] ]
def restore_database_dumps(databases, log_prefix, dry_run): def restore_database_dumps(databases, log_prefix, location_config, dry_run):
''' '''
Restore the given PostgreSQL databases from disk. The databases are supplied as a sequence of Restore the given PostgreSQL databases from disk. The databases are supplied as a sequence of
dicts, one dict describing each database as per the configuration schema. Use the given log dicts, one dict describing each database as per the configuration schema. Use the given log
prefix in any log entries. If this is a dry run, then don't actually restore anything. prefix in any log entries. Use the given location configuration dict to construct the
destination path. If this is a dry run, then don't actually restore anything.
''' '''
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 ''
for database in databases: for database in databases:
dump_filename = dump.make_database_dump_filename( dump_filename = dump.make_database_dump_filename(
DUMP_PATH, database['name'], database.get('hostname') make_dump_path(location_config), database['name'], database.get('hostname')
) )
restore_command = ( restore_command = (
('pg_restore', '--no-password', '--clean', '--if-exists', '--exit-on-error') ('pg_restore', '--no-password', '--clean', '--if-exists', '--exit-on-error')

View file

@ -23,8 +23,12 @@ hooks:
``` ```
Prior to each backup, borgmatic dumps each configured database to a file Prior to each backup, borgmatic dumps each configured database to a file
(located in `~/.borgmatic/`) and includes it in the backup. After the backup and includes it in the backup. After the backup completes, borgmatic removes
completes, borgmatic removes the database dump files to recover disk space. the database dump files to recover disk space.
borgmatic creates these temporary dump files in `~/.borgmatic` by default. To
customize this path, set the `borgmatic_source_directory` option in the
`location` section of borgmatic's configuration.
Here's a more involved example that connects to remote databases: Here's a more involved example that connects to remote databases:

View file

@ -1,6 +1,6 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
VERSION = '1.4.18' VERSION = '1.4.19'
setup( setup(

View file

@ -184,14 +184,21 @@ def test_borgmatic_source_directories_set_when_directory_exists():
flexmock(module.os.path).should_receive('exists').and_return(True) flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('expanduser') flexmock(module.os.path).should_receive('expanduser')
assert module.borgmatic_source_directories() == [module.BORGMATIC_SOURCE_DIRECTORY] assert module.borgmatic_source_directories('/tmp') == ['/tmp']
def test_borgmatic_source_directories_empty_when_directory_does_not_exist(): def test_borgmatic_source_directories_empty_when_directory_does_not_exist():
flexmock(module.os.path).should_receive('exists').and_return(False) flexmock(module.os.path).should_receive('exists').and_return(False)
flexmock(module.os.path).should_receive('expanduser') flexmock(module.os.path).should_receive('expanduser')
assert module.borgmatic_source_directories() == [] assert module.borgmatic_source_directories('/tmp') == []
def test_borgmatic_source_directories_defaults_when_directory_not_given():
flexmock(module.os.path).should_receive('exists').and_return(True)
flexmock(module.os.path).should_receive('expanduser')
assert module.borgmatic_source_directories(None) == [module.DEFAULT_BORGMATIC_SOURCE_DIRECTORY]
DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}' DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'

View file

@ -4,6 +4,14 @@ 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():
assert module.make_database_dump_path('/tmp', 'super_databases') == '/tmp/super_databases'
def test_make_database_dump_path_defaults_without_source_directory():
assert module.make_database_dump_path(None, 'super_databases') == '~/.borgmatic/super_databases'
def test_make_database_dump_filename_uses_name_and_hostname(): def test_make_database_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')

View file

@ -8,6 +8,7 @@ from borgmatic.hooks import mysql as module
def test_dump_databases_runs_mysqldump_for_each_database(): def test_dump_databases_runs_mysqldump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
output_file = flexmock() output_file = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
@ -21,23 +22,25 @@ def test_dump_databases_runs_mysqldump_for_each_database():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_with_dry_run_skips_mysqldump(): def test_dump_databases_with_dry_run_skips_mysqldump():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
flexmock(module.os).should_receive('makedirs').never() flexmock(module.os).should_receive('makedirs').never()
flexmock(module).should_receive('execute_command').never() flexmock(module).should_receive('execute_command').never()
module.dump_databases(databases, 'test.yaml', dry_run=True) module.dump_databases(databases, 'test.yaml', {}, dry_run=True)
def test_dump_databases_runs_mysqldump_with_hostname_and_port(): def test_dump_databases_runs_mysqldump_with_hostname_and_port():
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}] databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
output_file = flexmock() output_file = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/database.example.org/foo' 'databases/database.example.org/foo'
) )
@ -61,12 +64,13 @@ def test_dump_databases_runs_mysqldump_with_hostname_and_port():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_mysqldump_with_username_and_password(): def test_dump_databases_runs_mysqldump_with_username_and_password():
databases = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}] databases = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
output_file = flexmock() output_file = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -79,12 +83,13 @@ def test_dump_databases_runs_mysqldump_with_username_and_password():
extra_environment={'MYSQL_PWD': 'trustsome1'}, extra_environment={'MYSQL_PWD': 'trustsome1'},
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_mysqldump_with_options(): def test_dump_databases_runs_mysqldump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}] databases = [{'name': 'foo', 'options': '--stuff=such'}]
output_file = flexmock() output_file = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -97,12 +102,13 @@ def test_dump_databases_runs_mysqldump_with_options():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_mysqldump_for_all_databases(): def test_dump_databases_runs_mysqldump_for_all_databases():
databases = [{'name': 'all'}] databases = [{'name': 'all'}]
output_file = flexmock() output_file = flexmock()
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/all' 'databases/localhost/all'
) )
@ -115,30 +121,33 @@ def test_dump_databases_runs_mysqldump_for_all_databases():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_make_database_dump_patterns_converts_names_to_glob_paths(): def test_make_database_dump_patterns_converts_names_to_glob_paths():
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_database_dump_filename').and_return(
'databases/*/foo' 'databases/*/foo'
).and_return('databases/*/bar') ).and_return('databases/*/bar')
assert module.make_database_dump_patterns(flexmock(), flexmock(), ('foo', 'bar')) == [ assert module.make_database_dump_patterns(flexmock(), flexmock(), {}, ('foo', 'bar')) == [
'databases/*/foo', 'databases/*/foo',
'databases/*/bar', 'databases/*/bar',
] ]
def test_make_database_dump_patterns_treats_empty_names_as_matching_all_databases(): def test_make_database_dump_patterns_treats_empty_names_as_matching_all_databases():
flexmock(module).should_receive('make_dump_path').and_return('/dump/path')
flexmock(module.dump).should_receive('make_database_dump_filename').with_args( flexmock(module.dump).should_receive('make_database_dump_filename').with_args(
module.DUMP_PATH, '*', '*' '/dump/path', '*', '*'
).and_return('databases/*/*') ).and_return('databases/*/*')
assert module.make_database_dump_patterns(flexmock(), flexmock(), ()) == ['databases/*/*'] assert module.make_database_dump_patterns(flexmock(), flexmock(), {}, ()) == ['databases/*/*']
def test_restore_database_dumps_restores_each_database(): def test_restore_database_dumps_restores_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
@ -153,11 +162,12 @@ def test_restore_database_dumps_restores_each_database():
('mysql', '--batch'), input_file=input_file, extra_environment=None ('mysql', '--batch'), input_file=input_file, extra_environment=None
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)
def test_restore_database_dumps_runs_mysql_with_hostname_and_port(): def test_restore_database_dumps_runs_mysql_with_hostname_and_port():
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}] databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -182,11 +192,12 @@ def test_restore_database_dumps_runs_mysql_with_hostname_and_port():
extra_environment=None, extra_environment=None,
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)
def test_restore_database_dumps_runs_mysql_with_username_and_password(): def test_restore_database_dumps_runs_mysql_with_username_and_password():
databases = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}] databases = [{'name': 'foo', 'username': 'root', 'password': 'trustsome1'}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -202,4 +213,4 @@ def test_restore_database_dumps_runs_mysql_with_username_and_password():
extra_environment={'MYSQL_PWD': 'trustsome1'}, extra_environment={'MYSQL_PWD': 'trustsome1'},
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)

View file

@ -5,6 +5,7 @@ from borgmatic.hooks import postgresql as module
def test_dump_databases_runs_pg_dump_for_each_database(): def test_dump_databases_runs_pg_dump_for_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
@ -25,22 +26,24 @@ def test_dump_databases_runs_pg_dump_for_each_database():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_with_dry_run_skips_pg_dump(): def test_dump_databases_with_dry_run_skips_pg_dump():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
flexmock(module.os).should_receive('makedirs').never() flexmock(module.os).should_receive('makedirs').never()
flexmock(module).should_receive('execute_command').never() flexmock(module).should_receive('execute_command').never()
module.dump_databases(databases, 'test.yaml', dry_run=True) module.dump_databases(databases, 'test.yaml', {}, dry_run=True)
def test_dump_databases_runs_pg_dump_with_hostname_and_port(): def test_dump_databases_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}]
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_database_dump_filename').and_return(
'databases/database.example.org/foo' 'databases/database.example.org/foo'
) )
@ -64,11 +67,12 @@ def test_dump_databases_runs_pg_dump_with_hostname_and_port():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_pg_dump_with_username_and_password(): def test_dump_databases_runs_pg_dump_with_username_and_password():
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}] databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -90,11 +94,12 @@ def test_dump_databases_runs_pg_dump_with_username_and_password():
extra_environment={'PGPASSWORD': 'trustsome1'}, extra_environment={'PGPASSWORD': 'trustsome1'},
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_pg_dump_with_format(): def test_dump_databases_runs_pg_dump_with_format():
databases = [{'name': 'foo', 'format': 'tar'}] databases = [{'name': 'foo', 'format': 'tar'}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -114,11 +119,12 @@ def test_dump_databases_runs_pg_dump_with_format():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_pg_dump_with_options(): def test_dump_databases_runs_pg_dump_with_options():
databases = [{'name': 'foo', 'options': '--stuff=such'}] databases = [{'name': 'foo', 'options': '--stuff=such'}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -139,11 +145,12 @@ def test_dump_databases_runs_pg_dump_with_options():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_dump_databases_runs_pg_dumpall_for_all_databases(): def test_dump_databases_runs_pg_dumpall_for_all_databases():
databases = [{'name': 'all'}] databases = [{'name': 'all'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/all' 'databases/localhost/all'
) )
@ -154,30 +161,33 @@ def test_dump_databases_runs_pg_dumpall_for_all_databases():
extra_environment=None, extra_environment=None,
).once() ).once()
module.dump_databases(databases, 'test.yaml', dry_run=False) module.dump_databases(databases, 'test.yaml', {}, dry_run=False)
def test_make_database_dump_patterns_converts_names_to_glob_paths(): def test_make_database_dump_patterns_converts_names_to_glob_paths():
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_database_dump_filename').and_return(
'databases/*/foo' 'databases/*/foo'
).and_return('databases/*/bar') ).and_return('databases/*/bar')
assert module.make_database_dump_patterns(flexmock(), flexmock(), ('foo', 'bar')) == [ assert module.make_database_dump_patterns(flexmock(), flexmock(), {}, ('foo', 'bar')) == [
'databases/*/foo', 'databases/*/foo',
'databases/*/bar', 'databases/*/bar',
] ]
def test_make_database_dump_patterns_treats_empty_names_as_matching_all_databases(): def test_make_database_dump_patterns_treats_empty_names_as_matching_all_databases():
flexmock(module).should_receive('make_dump_path').and_return('/dump/path')
flexmock(module.dump).should_receive('make_database_dump_filename').with_args( flexmock(module.dump).should_receive('make_database_dump_filename').with_args(
module.DUMP_PATH, '*', '*' '/dump/path', '*', '*'
).and_return('databases/*/*') ).and_return('databases/*/*')
assert module.make_database_dump_patterns(flexmock(), flexmock(), ()) == ['databases/*/*'] assert module.make_database_dump_patterns(flexmock(), flexmock(), {}, ()) == ['databases/*/*']
def test_restore_database_dumps_restores_each_database(): def test_restore_database_dumps_restores_each_database():
databases = [{'name': 'foo'}, {'name': 'bar'}] databases = [{'name': 'foo'}, {'name': 'bar'}]
flexmock(module).should_receive('make_dump_path').and_return('')
flexmock(module.dump).should_receive('make_database_dump_filename').and_return( flexmock(module.dump).should_receive('make_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
).and_return('databases/localhost/bar') ).and_return('databases/localhost/bar')
@ -201,11 +211,12 @@ def test_restore_database_dumps_restores_each_database():
extra_environment=None, extra_environment=None,
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)
def test_restore_database_dumps_runs_pg_restore_with_hostname_and_port(): def test_restore_database_dumps_runs_pg_restore_with_hostname_and_port():
databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}] databases = [{'name': 'foo', 'hostname': 'database.example.org', 'port': 5433}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -244,11 +255,12 @@ def test_restore_database_dumps_runs_pg_restore_with_hostname_and_port():
extra_environment=None, extra_environment=None,
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)
def test_restore_database_dumps_runs_pg_restore_with_username_and_password(): def test_restore_database_dumps_runs_pg_restore_with_username_and_password():
databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}] databases = [{'name': 'foo', 'username': 'postgres', 'password': 'trustsome1'}]
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_database_dump_filename').and_return(
'databases/localhost/foo' 'databases/localhost/foo'
) )
@ -283,4 +295,4 @@ def test_restore_database_dumps_runs_pg_restore_with_username_and_password():
extra_environment={'PGPASSWORD': 'trustsome1'}, extra_environment={'PGPASSWORD': 'trustsome1'},
).once() ).once()
module.restore_database_dumps(databases, 'test.yaml', dry_run=False) module.restore_database_dumps(databases, 'test.yaml', {}, dry_run=False)