Skip to content

Commit e4bd48d

Browse files
committed
Use environment and pgpass to connect to PostgreSQL
Closes jazzband#384.
1 parent 9d1909c commit e4bd48d

File tree

1 file changed

+57
-34
lines changed

1 file changed

+57
-34
lines changed

dbbackup/db/postgresql.py

+57-34
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
1-
from urllib.parse import quote
1+
from tempfile import mkstemp
22
import logging
3+
import os
34

45
from .base import BaseCommandDBConnector
5-
from .exceptions import DumpError
66

77
logger = logging.getLogger('dbbackup.command')
88

99

10-
def create_postgres_uri(self):
11-
host = self.settings.get('HOST')
12-
if not host:
13-
raise DumpError('A host name is required')
14-
15-
dbname = self.settings.get('NAME') or ''
16-
user = quote(self.settings.get('USER') or '')
17-
password = self.settings.get('PASSWORD') or ''
18-
password = ':{}'.format(quote(password)) if password else ''
19-
if not user:
20-
password = ''
21-
else:
22-
host = '@' + host
23-
24-
port = ':{}'.format(self.settings.get('PORT')) if self.settings.get('PORT') else ''
25-
dbname = f'--dbname=postgresql://{user}{password}{host}{port}/{dbname}'
26-
return dbname
10+
class PgEnvWrapper:
11+
"""
12+
Context manager that updates the OS environment with the libpq variables
13+
derived from settings, and if necessary a temporary .pgpass file.
14+
"""
15+
def __init__(self, settings):
16+
self.settings = settings
17+
self.pgpass_path = None
18+
19+
def __enter__(self):
20+
# Get all settings, with empty defaults to detect later
21+
pghost = self.settings.get('HOST', None)
22+
pgport = self.settings.get('PORT', None)
23+
pguser = self.settings.get('USER', None)
24+
pgdatabase = self.settings.get('NAME', None)
25+
pgpassword = self.settings.get('PASSWORD', None)
26+
27+
# Set PG* environment variables for everything we got
28+
# All defaults are thus left to libpq
29+
env = os.environ.copy()
30+
if pghost:
31+
env['PGHOST'] = pghost
32+
if pgport:
33+
env['PGPORT'] = pgport
34+
if pguser:
35+
env['PGUSER'] = pguser
36+
if pgdatabase:
37+
env['PGDATABASE'] = pgdatabase
38+
39+
if pgpassword:
40+
# Open a temporary file (safe name, mode 600) as .pgpass file
41+
fd, self.pgpass_path = mkstemp(text=True)
42+
os.close(fd)
43+
with open(self.pgpass_path, 'w') as pgpass_file:
44+
# Write a catch-all entry, as this .pgass is only used once and by us
45+
pgpass_file.write(f'*:*:*:*:{pgpassword}\n')
46+
env['PGPASSFILE'] = self.pgpass_path
47+
48+
return env
49+
50+
def __exit__(self, *args):
51+
if self.pgpass_path:
52+
os.unlink(self.pgpass_path)
2753

2854

2955
class PgDumpConnector(BaseCommandDBConnector):
@@ -39,28 +65,27 @@ class PgDumpConnector(BaseCommandDBConnector):
3965

4066
def _create_dump(self):
4167
cmd = '{} '.format(self.dump_cmd)
42-
cmd = cmd + create_postgres_uri(self)
4368

4469
for table in self.exclude:
4570
cmd += ' --exclude-table-data={}'.format(table)
4671
if self.drop:
4772
cmd += ' --clean'
4873

4974
cmd = '{} {} {}'.format(self.dump_prefix, cmd, self.dump_suffix)
50-
stdout, stderr = self.run_command(cmd, env=self.dump_env)
75+
with PgEnvWrapper(self.settings) as env:
76+
stdout, stderr = self.run_command(cmd, env={**self.dump_env, **env})
5177
return stdout
5278

5379
def _restore_dump(self, dump):
5480
cmd = '{} '.format(self.restore_cmd)
55-
cmd = cmd + create_postgres_uri(self)
5681

5782
# without this, psql terminates with an exit value of 0 regardless of errors
5883
cmd += ' --set ON_ERROR_STOP=on'
5984
if self.single_transaction:
6085
cmd += ' --single-transaction'
61-
cmd += ' {}'.format(self.settings['NAME'])
6286
cmd = '{} {} {}'.format(self.restore_prefix, cmd, self.restore_suffix)
63-
stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
87+
with PgEnvWrapper(self.settings) as env:
88+
stdout, stderr = self.run_command(cmd, stdin=dump, env={**self.restore_env, **env})
6489
return stdout, stderr
6590

6691

@@ -76,11 +101,8 @@ def _enable_postgis(self):
76101
self.psql_cmd)
77102
cmd += ' --username={}'.format(self.settings['ADMIN_USER'])
78103
cmd += ' --no-password'
79-
if self.settings.get('HOST'):
80-
cmd += ' --host={}'.format(self.settings['HOST'])
81-
if self.settings.get('PORT'):
82-
cmd += ' --port={}'.format(self.settings['PORT'])
83-
return self.run_command(cmd)
104+
with PgEnvWrapper(self.settings) as env:
105+
return self.run_command(cmd, env=env)
84106

85107
def _restore_dump(self, dump):
86108
if self.settings.get('ADMIN_USER'):
@@ -101,23 +123,24 @@ class PgDumpBinaryConnector(PgDumpConnector):
101123

102124
def _create_dump(self):
103125
cmd = '{} '.format(self.dump_cmd)
104-
cmd = cmd + create_postgres_uri(self)
105126

106127
cmd += ' --format=custom'
107128
for table in self.exclude:
108129
cmd += ' --exclude-table-data={}'.format(table)
109130
cmd = '{} {} {}'.format(self.dump_prefix, cmd, self.dump_suffix)
110-
stdout, stderr = self.run_command(cmd, env=self.dump_env)
131+
with PgEnvWrapper(self.settings) as env:
132+
stdout, stderr = self.run_command(cmd, env={**self.dump_env, **env})
111133
return stdout
112134

113135
def _restore_dump(self, dump):
114-
dbname = create_postgres_uri(self)
115-
cmd = '{} {}'.format(self.restore_cmd, dbname)
136+
cmd = '{} '.format(self.restore_cmd)
116137

117138
if self.single_transaction:
118139
cmd += ' --single-transaction'
119140
if self.drop:
120141
cmd += ' --clean'
142+
cmd += '-d {}'.format(self.settings.get('NAME'))
121143
cmd = '{} {} {}'.format(self.restore_prefix, cmd, self.restore_suffix)
122-
stdout, stderr = self.run_command(cmd, stdin=dump, env=self.restore_env)
144+
with PgEnvWrapper(self.settings) as env:
145+
stdout, stderr = self.run_command(cmd, stdin=dump, env={**self.restore_env, **env})
123146
return stdout, stderr

0 commit comments

Comments
 (0)