Skip to content

Commit 114dec1

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

File tree

1 file changed

+56
-33
lines changed

1 file changed

+56
-33
lines changed

dbbackup/db/postgresql.py

Lines changed: 56 additions & 33 deletions
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
56
from .exceptions import DumpError
67

78
logger = logging.getLogger('dbbackup.command')
89

910

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