borgmatic/borgmatic/hooks/mongodb.py

180 lines
7.0 KiB
Python
Raw Normal View History

2021-12-26 00:00:58 +00:00
import logging
from borgmatic.execute import execute_command, execute_command_with_processes
from borgmatic.hooks import dump
logger = logging.getLogger(__name__)
2023-07-09 06:14:30 +00:00
def make_dump_path(config): # pragma: no cover
2021-12-26 00:00:58 +00:00
'''
2023-07-09 06:14:30 +00:00
Make the dump path from the given configuration dict and the name of this hook.
2021-12-26 00:00:58 +00:00
'''
return dump.make_database_dump_path(
2023-07-09 06:14:30 +00:00
config.get('borgmatic_source_directory'), 'mongodb_databases'
2021-12-26 00:00:58 +00:00
)
2023-07-09 06:14:30 +00:00
def dump_databases(databases, config, log_prefix, dry_run):
2021-12-26 00:00:58 +00:00
'''
Dump the given MongoDB databases to a named pipe. The databases are supplied as a sequence of
2023-07-09 06:14:30 +00:00
dicts, one dict describing each database as per the configuration schema. Use the configuration
dict to construct the destination path and the given log prefix in any log entries.
2021-12-26 00:00:58 +00:00
Return a sequence of subprocess.Popen instances for the dump processes ready to spew to a named
pipe. But if this is a dry run, then don't actually dump anything and return an empty sequence.
'''
dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
logger.info(f'{log_prefix}: Dumping MongoDB databases{dry_run_label}')
2021-12-26 00:00:58 +00:00
processes = []
for database in databases:
name = database['name']
dump_filename = dump.make_database_dump_filename(
2023-07-09 06:14:30 +00:00
make_dump_path(config), name, database.get('hostname')
2021-12-26 00:00:58 +00:00
)
2021-12-29 21:18:50 +00:00
dump_format = database.get('format', 'archive')
2021-12-26 00:00:58 +00:00
logger.debug(
f'{log_prefix}: Dumping MongoDB database {name} to {dump_filename}{dry_run_label}',
2021-12-26 00:00:58 +00:00
)
if dry_run:
continue
command = build_dump_command(database, dump_filename, dump_format)
2021-12-26 00:00:58 +00:00
if dump_format == 'directory':
dump.create_parent_directory_for_dump(dump_filename)
execute_command(command, shell=True)
2021-12-26 00:00:58 +00:00
else:
dump.create_named_pipe_for_dump(dump_filename)
processes.append(execute_command(command, shell=True, run_to_completion=False))
2021-12-26 00:00:58 +00:00
return processes
def build_dump_command(database, dump_filename, dump_format):
'''
Return the mongodump command from a single database configuration.
'''
all_databases = database['name'] == 'all'
command = ['mongodump']
2021-12-26 00:00:58 +00:00
if dump_format == 'directory':
command.extend(('--out', dump_filename))
2021-12-26 00:00:58 +00:00
if 'hostname' in database:
command.extend(('--host', database['hostname']))
if 'port' in database:
command.extend(('--port', str(database['port'])))
if 'username' in database:
command.extend(('--username', database['username']))
if 'password' in database:
command.extend(('--password', database['password']))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
2021-12-26 00:00:58 +00:00
if not all_databases:
command.extend(('--db', database['name']))
if 'options' in database:
command.extend(database['options'].split(' '))
if dump_format != 'directory':
command.extend(('--archive', '>', dump_filename))
2021-12-26 00:00:58 +00:00
return command
2023-07-09 06:14:30 +00:00
def remove_database_dumps(databases, config, log_prefix, dry_run): # pragma: no cover
2021-12-26 00:00:58 +00:00
'''
Remove all database dump files for this hook regardless of the given databases. Use the log
2023-07-09 06:14:30 +00:00
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.
2021-12-26 00:00:58 +00:00
'''
2023-07-09 06:14:30 +00:00
dump.remove_database_dumps(make_dump_path(config), 'MongoDB', log_prefix, dry_run)
2021-12-26 00:00:58 +00:00
2023-07-09 06:14:30 +00:00
def make_database_dump_pattern(databases, config, log_prefix, name=None): # pragma: no cover
2021-12-26 00:00:58 +00:00
'''
2023-07-09 06:14:30 +00:00
Given a sequence of database configurations dicts, a configuration dict, a prefix to log with,
2021-12-26 00:00:58 +00:00
and a database name to match, return the corresponding glob patterns to match the database dump
in an archive.
'''
2023-07-09 06:14:30 +00:00
return dump.make_database_dump_filename(make_dump_path(config), name, hostname='*')
2021-12-26 00:00:58 +00:00
def restore_database_dump(
2023-07-09 06:14:30 +00:00
database_config, config, log_prefix, dry_run, extract_process, connection_params
):
2021-12-26 00:00:58 +00:00
'''
Restore the given MongoDB database from an extract stream. The database is supplied as a
one-element sequence containing a dict describing the database, as per the configuration schema.
2023-07-09 06:14:30 +00:00
Use the configuration 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 restore anything. Trigger the given active
extract process (an instance of subprocess.Popen) to produce output to consume.
2021-12-26 00:00:58 +00:00
If the extract process is None, then restore the dump from the filesystem rather than from an
extract stream.
'''
dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else ''
if len(database_config) != 1:
raise ValueError('The database configuration value is invalid')
database = database_config[0]
dump_filename = dump.make_database_dump_filename(
2023-07-09 06:14:30 +00:00
make_dump_path(config), database['name'], database.get('hostname')
2021-12-26 00:00:58 +00:00
)
restore_command = build_restore_command(
extract_process, database, dump_filename, connection_params
)
2021-12-26 00:00:58 +00:00
logger.debug(f"{log_prefix}: Restoring MongoDB database {database['name']}{dry_run_label}")
2021-12-26 00:00:58 +00:00
if dry_run:
return
# Don't give Borg local path so as to error on warnings, as "borg extract" only gives a warning
# if the restore paths don't exist in the archive.
2021-12-26 00:00:58 +00:00
execute_command_with_processes(
restore_command,
[extract_process] if extract_process else [],
output_log_level=logging.DEBUG,
input_file=extract_process.stdout if extract_process else None,
)
def build_restore_command(extract_process, database, dump_filename, connection_params):
2021-12-26 00:00:58 +00:00
'''
Return the mongorestore command from a single database configuration.
'''
hostname = connection_params['hostname'] or database.get(
'restore_hostname', database.get('hostname')
)
port = str(connection_params['port'] or database.get('restore_port', database.get('port', '')))
username = connection_params['username'] or database.get(
'restore_username', database.get('username')
)
password = connection_params['password'] or database.get(
'restore_password', database.get('password')
)
command = ['mongorestore']
if extract_process:
command.append('--archive')
else:
command.extend(('--dir', dump_filename))
2021-12-26 00:00:58 +00:00
if database['name'] != 'all':
command.extend(('--drop', '--db', database['name']))
if hostname:
command.extend(('--host', hostname))
if port:
command.extend(('--port', str(port)))
if username:
command.extend(('--username', username))
if password:
command.extend(('--password', password))
if 'authentication_database' in database:
command.extend(('--authenticationDatabase', database['authentication_database']))
if 'restore_options' in database:
command.extend(database['restore_options'].split(' '))
2023-04-12 04:04:19 +00:00
if database['schemas']:
for schema in database['schemas']:
command.extend(('--nsInclude', schema))
2021-12-26 00:00:58 +00:00
return command