From abd47fc14e338c984ed01a5f79be9a91a797d5de Mon Sep 17 00:00:00 2001 From: Edward Shornock Date: Thu, 18 Jun 2020 01:16:34 +0300 Subject: [PATCH] Add SSL support to PostgreSQL hooks --- borgmatic/config/schema.yaml | 28 +++++++++++++++++++++++++ borgmatic/hooks/postgresql.py | 27 ++++++++++++++++++++++-- tests/unit/hooks/test_postgresql.py | 32 ++++++++++++++--------------- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml index cdd54eaed..210148bb8 100644 --- a/borgmatic/config/schema.yaml +++ b/borgmatic/config/schema.yaml @@ -556,6 +556,34 @@ map: documentation for details. Note that format is ignored when the database name is "all". example: directory + sslmode: + type: str + enum: ['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full'] + desc: | + SSL mode to use to connect to the database server. One of "disable", "allow", "prefer", + "require", "verify-ca" or "verify-full". Defaults to "disable". See + https://www.postgresql.org/docs/current/libpq-ssl.html for details. + example: disable + sslcert: + type: str + desc: | + Path to a client certificate. + example: "/root/.postgresql/postgresql.crt" + sslkey: + type: str + desc: | + Path to a private client key. + example: "/root/.postgresql/postgresql.key" + sslrootcert: + type: str + desc: | + Path to a root certificate containing a list of trusted certificate authorities. + example: "/root/.postgresql/root.crt" + sslcrl: + type: str + desc: | + Path to a certificate revocation list. + example: "/root/.postgresql/root.crl" options: type: str desc: | diff --git a/borgmatic/hooks/postgresql.py b/borgmatic/hooks/postgresql.py index db90025e2..c9a4fdaa4 100644 --- a/borgmatic/hooks/postgresql.py +++ b/borgmatic/hooks/postgresql.py @@ -56,7 +56,19 @@ def dump_databases(databases, log_prefix, location_config, dry_run): # format in a particular, a named destination is required, and redirection doesn't work. + (('>', dump_filename) if dump_format != 'directory' else ()) ) - extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None + extra_environment = dict() + if 'password' in database: + extra_environment['PGPASSWORD'] = database['password'] + extra_environment['PGSSLMODE'] = database['sslmode'] if 'sslmode' in database else 'disable' + if 'sslcert' in database: + extra_environment['PGSSLCERT'] = database['sslcert'] + if 'sslkey' in database: + extra_environment['PGSSLKEY'] = database['sslkey'] + if 'sslrootcert' in database: + extra_environment['PGSSLROOTCERT'] = database['sslrootcert'] + if 'sslcrl' in database: + extra_environment['PGSSLCRL'] = database['sslcrl'] + logger.debug( '{}: Dumping PostgreSQL database {} to {}{}'.format( @@ -141,7 +153,18 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run, + (('--username', database['username']) if 'username' in database else ()) + (() if extract_process else (dump_filename,)) ) - extra_environment = {'PGPASSWORD': database['password']} if 'password' in database else None + extra_environment = dict() + if 'password' in database: + extra_environment['PGPASSWORD'] = database['password'] + extra_environment['PGSSLMODE'] = database['sslmode'] if 'sslmode' in database else 'disable' + if 'sslcert' in database: + extra_environment['PGSSLCERT'] = database['sslcert'] + if 'sslkey' in database: + extra_environment['PGSSLKEY'] = database['sslkey'] + if 'sslrootcert' in database: + extra_environment['PGSSLROOTCERT'] = database['sslrootcert'] + if 'sslcrl' in database: + extra_environment['PGSSLCRL'] = database['sslcrl'] logger.debug( '{}: Restoring PostgreSQL database {}{}'.format(log_prefix, database['name'], dry_run_label) diff --git a/tests/unit/hooks/test_postgresql.py b/tests/unit/hooks/test_postgresql.py index 0f3d70dcb..5b92a8728 100644 --- a/tests/unit/hooks/test_postgresql.py +++ b/tests/unit/hooks/test_postgresql.py @@ -29,7 +29,7 @@ def test_dump_databases_runs_pg_dump_for_each_database(): 'databases/localhost/{}'.format(name), ), shell=True, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -74,7 +74,7 @@ def test_dump_databases_runs_pg_dump_with_hostname_and_port(): 'databases/database.example.org/foo', ), shell=True, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -105,7 +105,7 @@ def test_dump_databases_runs_pg_dump_with_username_and_password(): 'databases/localhost/foo', ), shell=True, - extra_environment={'PGPASSWORD': 'trustsome1'}, + extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -135,7 +135,7 @@ def test_dump_databases_runs_pg_dump_with_directory_format(): 'foo', ), shell=True, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -165,7 +165,7 @@ def test_dump_databases_runs_pg_dump_with_options(): 'databases/localhost/foo', ), shell=True, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -184,7 +184,7 @@ def test_dump_databases_runs_pg_dumpall_for_all_databases(): flexmock(module).should_receive('execute_command').with_args( ('pg_dumpall', '--no-password', '--clean', '--if-exists', '>', 'databases/localhost/all'), shell=True, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, run_to_completion=False, ).and_return(process).once() @@ -210,12 +210,12 @@ def test_restore_database_dump_runs_pg_restore(): processes=[extract_process], output_log_level=logging.DEBUG, input_file=extract_process.stdout, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, borg_local_path='borg', ).once() flexmock(module).should_receive('execute_command').with_args( ('psql', '--no-password', '--quiet', '--dbname', 'foo', '--command', 'ANALYZE'), - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, ).once() module.restore_database_dump( @@ -260,7 +260,7 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port(): processes=[extract_process], output_log_level=logging.DEBUG, input_file=extract_process.stdout, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, borg_local_path='borg', ).once() flexmock(module).should_receive('execute_command').with_args( @@ -277,7 +277,7 @@ def test_restore_database_dump_runs_pg_restore_with_hostname_and_port(): '--command', 'ANALYZE', ), - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, ).once() module.restore_database_dump( @@ -306,7 +306,7 @@ def test_restore_database_dump_runs_pg_restore_with_username_and_password(): processes=[extract_process], output_log_level=logging.DEBUG, input_file=extract_process.stdout, - extra_environment={'PGPASSWORD': 'trustsome1'}, + extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}, borg_local_path='borg', ).once() flexmock(module).should_receive('execute_command').with_args( @@ -321,7 +321,7 @@ def test_restore_database_dump_runs_pg_restore_with_username_and_password(): '--command', 'ANALYZE', ), - extra_environment={'PGPASSWORD': 'trustsome1'}, + extra_environment={'PGPASSWORD': 'trustsome1', 'PGSSLMODE': 'disable'}, ).once() module.restore_database_dump( @@ -340,11 +340,11 @@ def test_restore_database_dump_runs_psql_for_all_database_dump(): processes=[extract_process], output_log_level=logging.DEBUG, input_file=extract_process.stdout, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, borg_local_path='borg', ).once() flexmock(module).should_receive('execute_command').with_args( - ('psql', '--no-password', '--quiet', '--command', 'ANALYZE'), extra_environment=None + ('psql', '--no-password', '--quiet', '--command', 'ANALYZE'), extra_environment={'PGSSLMODE': 'disable'} ).once() module.restore_database_dump( @@ -383,12 +383,12 @@ def test_restore_database_dump_without_extract_process_restores_from_disk(): processes=[], output_log_level=logging.DEBUG, input_file=None, - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, borg_local_path='borg', ).once() flexmock(module).should_receive('execute_command').with_args( ('psql', '--no-password', '--quiet', '--dbname', 'foo', '--command', 'ANALYZE'), - extra_environment=None, + extra_environment={'PGSSLMODE': 'disable'}, ).once() module.restore_database_dump(