diff --git a/borgmatic/actions/restore.py b/borgmatic/actions/restore.py index ded83f4f..a701aa0d 100644 --- a/borgmatic/actions/restore.py +++ b/borgmatic/actions/restore.py @@ -68,15 +68,21 @@ def restore_single_database( archive_name, hook_name, database, + connection_params, ): # pragma: no cover ''' - Given (among other things) an archive name, a database hook name, and a configured database + Given (among other things) an archive name, a database hook name, the hostname, + port, username and password as connection params, and a configured database configuration dict, restore that database from the archive. ''' logger.info( f'{repository.get("label", repository["path"])}: Restoring database {database["name"]}' ) + logger.info( + f'hostname port username password for database {database["name"]}' + ) + dump_pattern = borgmatic.hooks.dispatch.call_hooks( 'make_database_dump_pattern', hooks, @@ -113,6 +119,7 @@ def restore_single_database( location, global_arguments.dry_run, extract_process, + connection_params, ) @@ -308,6 +315,13 @@ def run_restore( hooks, archive_database_names, hook_name, database_name ) + connection_params = { + 'hostname': restore_arguments.hostname, + 'port': restore_arguments.port, + 'username': restore_arguments.username, + 'password': restore_arguments.password, + } + if not found_database: remaining_restore_names.setdefault(found_hook_name or hook_name, []).append( database_name @@ -327,6 +341,7 @@ def run_restore( archive_name, found_hook_name or hook_name, dict(found_database, **{'schemas': restore_arguments.schemas}), + connection_params, ) # For any database that weren't found via exact matches in the hooks configuration, try to diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index 6039e4fa..02f2fc3e 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -720,6 +720,21 @@ def make_parsers(): dest='schemas', help='Names of schemas to restore from the database, defaults to all schemas. Schemas are only supported for PostgreSQL and MongoDB databases', ) + restore_group.add_argument( + '--hostname', + help='Database hostname to restore to. Defaults to the "restore_hostname" option in borgmatic\'s configuration', + ) + restore_group.add_argument( + '--port', help='Port to restore to. Defaults to the "restore_port" option in borgmatic\'s configuration' + ) + restore_group.add_argument( + '--username', + help='Username with which to connect to the database. Defaults to the "restore_username" option in borgmatic\'s configuration', + ) + restore_group.add_argument( + '--password', + help='Password with which to connect to the restore database. Defaults to the "restore_password" option in borgmatic\'s configuration', + ) restore_group.add_argument( '-h', '--help', action='help', help='Show this help message and exit' ) diff --git a/borgmatic/hooks/postgresql.py b/borgmatic/hooks/postgresql.py index 95ec4ace..89ade66f 100644 --- a/borgmatic/hooks/postgresql.py +++ b/borgmatic/hooks/postgresql.py @@ -22,8 +22,7 @@ def make_dump_path(location_config): # pragma: no cover location_config.get('borgmatic_source_directory'), 'postgresql_databases' ) - -def make_extra_environment(database, restore=False): +def make_extra_environment(database, restore=False, connection_params=None): ''' Make the extra_environment dict from the given database configuration. ''' @@ -32,6 +31,8 @@ def make_extra_environment(database, restore=False): extra['PGPASSWORD'] = database['password'] if restore and 'restore_password' in database: extra['PGPASSWORD'] = database['restore_password'] + if connection_params is not None and connection_params.get('password'): + extra['PGPASSWORD'] = connection_params['password'] extra['PGSSLMODE'] = database.get('ssl_mode', 'disable') if 'ssl_cert' in database: extra['PGSSLCERT'] = database['ssl_cert'] @@ -195,7 +196,7 @@ def make_database_dump_pattern( return dump.make_database_dump_filename(make_dump_path(location_config), name, hostname='*') -def restore_database_dump(database_config, log_prefix, location_config, dry_run, extract_process): +def restore_database_dump(database_config, log_prefix, location_config, dry_run, extract_process, connection_params): ''' Restore the given PostgreSQL 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. @@ -205,6 +206,9 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, If the extract process is None, then restore the dump from the filesystem rather than from an extract stream. + + Use the given connection parameters to connect to the database. The connection parameters are + hostname, port, username, and password. ''' dry_run_label = ' (dry run; not actually restoring anything)' if dry_run else '' @@ -212,6 +216,12 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, raise ValueError('The database configuration value is invalid') database = database_config[0] + + 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')) + all_databases = bool(database['name'] == 'all') dump_filename = dump.make_database_dump_filename( make_dump_path(location_config), database['name'], database.get('hostname') @@ -220,9 +230,9 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, analyze_command = ( tuple(psql_command) + ('--no-password', '--no-psqlrc', '--quiet') - + (('--host', database.get('restore_hostname', database.get('hostname'))) if 'hostname' in database else ()) - + (('--port', str(database.get('restore_port', database.get('port')))) if 'port' in database else ()) - + (('--username', database.get('restore_username', database.get('username'))) if 'username' in database else ()) + + (('--host', hostname) if 'hostname' in database or 'restore_hostname' in database or 'hostname' in connection_params else ()) + + (('--port', port) if 'port' in database or 'restore_port' in database or 'port' in connection_params else ()) + + (('--username', username) if 'username' in database or 'restore_username' in database or 'username' in connection_params else ()) + (('--dbname', database['name']) if not all_databases else ()) + (tuple(database['analyze_options'].split(' ')) if 'analyze_options' in database else ()) + ('--command', 'ANALYZE') @@ -234,9 +244,9 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, + ('--no-password',) + (('--no-psqlrc',) if use_psql_command else ('--if-exists', '--exit-on-error', '--clean')) + (('--dbname', database['name']) if not all_databases else ()) - + (('--host', database.get('restore_hostname', database.get('hostname'))) if 'hostname' in database or 'restore_hostname' in database else ()) - + (('--port', str(database.get('restore_port', database.get('port')))) if 'port' in database or 'restore_port' in database else ()) - + (('--username', database.get('restore_username', database.get('username'))) if 'username' in database or 'restore_username' in database else ()) + + (('--host', hostname) if 'hostname' in database or 'restore_hostname' in database or 'hostname' in connection_params else ()) + + (('--port', port) if 'port' in database or 'restore_port' in database or 'port' in connection_params else ()) + + (('--username', username) if 'username' in database or 'restore_username' in database or 'username' in connection_params else ()) + (('--no-owner',) if database['no_owner'] else ()) + (tuple(database['restore_options'].split(' ')) if 'restore_options' in database else ()) + (() if extract_process else (dump_filename,)) @@ -247,7 +257,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, ) ) - extra_environment = make_extra_environment(database, restore=True) + extra_environment = make_extra_environment(database, restore=True, connection_params=connection_params) logger.debug(f"{log_prefix}: Restoring PostgreSQL database {database['name']}{dry_run_label}") if dry_run: