Skip to content

Commit dcea551

Browse files
author
James William Pye
committed
Add Installation interface for getting information about a PostgreSQL
installation. - Alter pg.cluster to use installation. - Add Installation ABC to api. - Place cluster errors under "Error". - move pg.pg_config into postgresql.installation - Fix some syntax errors in configfile.
1 parent 3193702 commit dcea551

6 files changed

Lines changed: 345 additions & 112 deletions

File tree

postgresql/api.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import warnings
2323
import collections
2424
from abc import ABCMeta, abstractproperty, abstractmethod
25-
from operator import attrgetter, methodcaller, itemgetter
25+
from operator import methodcaller, itemgetter
2626

2727
class docstr(object):
2828
"""
@@ -1304,6 +1304,38 @@ def __init__(self):
13041304
"""
13051305
self.ife_connect(self.handle_warnings_and_messages)
13061306

1307+
class Installation(InterfaceElement):
1308+
"""
1309+
Interface to a PostgreSQL installation. Instances would provide various
1310+
information about an installation of PostgreSQL accessible by the Python
1311+
"""
1312+
ife_label = "INSTALLATION"
1313+
1314+
@apdoc
1315+
@abstractproperty
1316+
def version(self):
1317+
"""
1318+
A version string consistent with what `SELECT version()` would output.
1319+
"""
1320+
1321+
@apdoc
1322+
@abstractproperty
1323+
def version_info(self):
1324+
"""
1325+
A tuple specifying the version in a form similar to Python's
1326+
sys.version_info. (8, 3, 3, 'final', 0)
1327+
1328+
See `postgresql.versionstring`.
1329+
"""
1330+
1331+
@apdoc
1332+
@abstractproperty
1333+
def type(self):
1334+
"""
1335+
The "type" of PostgreSQL. Normally, the first component of the string
1336+
returned by pg_config.
1337+
"""
1338+
13071339
class Cluster(InterfaceElement):
13081340
"""
13091341
Interface to a PostgreSQL cluster--a data directory. An implementation of
@@ -1312,6 +1344,13 @@ class Cluster(InterfaceElement):
13121344
ife_label = 'CLUSTER'
13131345
ife_ancestor = None
13141346

1347+
@apdoc
1348+
@abstractproperty
1349+
def installation(self) -> Installation:
1350+
"""
1351+
The installation used by the cluster.
1352+
"""
1353+
13151354
@abstractmethod
13161355
def init(self,
13171356
initdb : "path to the initdb to use" = None,

postgresql/cluster.py

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
import warnings
1919
import tempfile
2020
from contextlib import closing
21+
from operator import attrgetter
2122

2223
from . import api as pg_api
2324
from . import configfile
24-
from . import pg_config
25+
from . import installation as pg_inn
2526
from . import exceptions as pg_exc
2627
from . import driver as pg_driver
2728

29+
DEFAULT_CLUSTER_ENCODING = 'utf-8'
2830
DEFAULT_CONFIG_FILENAME = 'postgresql.conf'
2931
DEFAULT_HBA_FILENAME = 'pg_hba.conf'
3032
DEFAULT_PID_FILENAME = 'postmaster.pid'
@@ -38,7 +40,7 @@
3840
'numeric' : '--lc-numeric',
3941
'time' : '--lc-time',
4042
'authentication' : '-A',
41-
'superusername' : '-U',
43+
'user' : '-U',
4244
}
4345

4446
class Cluster(pg_api.Cluster):
@@ -48,12 +50,29 @@ class Cluster(pg_api.Cluster):
4850
Provides mechanisms to start, stop, restart, kill, drop, and configure a
4951
cluster(data directory).
5052
"""
53+
installation = None
54+
DEFAULT_CLUSTER_ENCODING = DEFAULT_CLUSTER_ENCODING
55+
DEFAULT_CONFIG_FILENAME = DEFAULT_CONFIG_FILENAME
56+
DEFAULT_PID_FILENAME = DEFAULT_PID_FILENAME
57+
DEFAULT_HBA_FILENAME = DEFAULT_HBA_FILENAME
58+
59+
ife_ancestor = property(attrgetter('installation'))
60+
def ife_snapshot_text(self):
61+
return self.data_directory + ' [' + (
62+
'running: ' + str(self.get_pid_from_file())
63+
if self.running() else 'not running'
64+
) + ']'
65+
66+
@property
67+
def daemon_path(self):
68+
return self.installation.postmaster or self.installation.postgres
69+
5170
def get_pid_from_file(self):
5271
"""
5372
The current pid from the postmaster.pid file.
5473
"""
5574
try:
56-
with closing(open(os.path.join(self.data_directory, DEFAULT_PID_FILENAME))) as f:
75+
with open(os.path.join(self.data_directory, self.DEFAULT_PID_FILENAME)) as f:
5776
return int(f.readline())
5877
except IOError as e:
5978
if e.errno in (errno.EIO, errno.ENOENT):
@@ -69,24 +88,21 @@ def settings(self):
6988
def hba_file(self):
7089
return self.settings.get(
7190
'hba_file',
72-
os.path.join(self.data_directory, DEFAULT_HBA_FILENAME)
91+
os.path.join(self.data_directory, self.DEFAULT_HBA_FILENAME)
7392
)
7493

94+
@classmethod
95+
def from_pg_config_path(type, data_directory, pg_config_path):
96+
"Create the cluster using the data_directory and the *path* to pg_config"
97+
return type(data_directory, pg_inn.Installation(pg_config_path))
98+
7599
def __init__(self,
76100
data_directory : "path to the data directory",
77-
pg_config_path : "path to pg_config to use" = 'pg_config',
78-
pg_config_data : "pg_config data to use; uses _path if None" = None
101+
installation : "postgresql.installation.Installation",
79102
):
80103
self.data_directory = os.path.abspath(data_directory)
81-
self.pgsql_dot_conf = os.path.join(data_directory, DEFAULT_CONFIG_FILENAME)
82-
if pg_config_data is None:
83-
self.config = pg_config.dictionary(pg_config_path)
84-
else:
85-
self.config = pg_config_data
86-
87-
self.postgres_path = os.path.join(self.config['bindir'], 'postmaster')
88-
if not os.path.exists(self.postgres_path):
89-
self.postgres_path = os.path.join(self.config['bindir'], 'postgres')
104+
self.installation = installation
105+
self.pgsql_dot_conf = os.path.join(data_directory, self.DEFAULT_CONFIG_FILENAME)
90106
self.daemon_process = None
91107
self.last_known_pid = self.get_pid_from_file()
92108

@@ -95,13 +111,12 @@ def __repr__(self):
95111
type(self).__module__,
96112
type(self).__name__,
97113
self.data_directory,
98-
self.postgres_path,
114+
self.installation,
99115
)
100116

101117
def init(self,
102-
initdb : "explicitly state the initdb binary to use" = None,
103-
verbose = False,
104-
superuserpass = None,
118+
password : "Password to assign to the cluster's superuser(`user` keyword)." = None,
119+
initdb : "[BEWARE] explicitly state the initdb binary to use" = None,
105120
**kw
106121
):
107122
"""
@@ -111,7 +126,16 @@ def init(self,
111126
`command_option_map` provides the mapping of keyword arguments
112127
to command options.
113128
"""
129+
if initdb is None:
130+
initdb = self.installation.initdb
131+
if initdb is None:
132+
raise pg_exc.ClusterInitializationError(
133+
"unable to find `initdb` executable for installation: " + \
134+
repr(self.installation)
135+
)
136+
114137
# Transform keyword options into command options for the executable.
138+
kw.setdefault('encoding', self.DEFAULT_CLUSTER_ENCODING)
115139
opts = []
116140
for x in kw:
117141
if x in ('logfile', 'extra_arguments'):
@@ -124,14 +148,16 @@ def init(self,
124148
extra_args = tuple([
125149
str(x) for x in kw.get('extra_arguments', ())
126150
])
127-
verbose = (initdb_option_map['verbose'],) if verbose is False else ()
128-
if superuserpass is not None:
129-
pass
130151

131-
if initdb is None:
132-
initdb = os.path.join(self.config['bindir'], 'initdb')
152+
supw_file = ()
153+
if password is not None:
154+
# got a superuserpass, it's
155+
supw_tmp = tempfile.NamedTemporaryFile(mode = 'w', encoding = kw['encoding'])
156+
supw_tmp.write(password)
157+
supw_tmp.flush()
158+
supw_file = ('--pwfile=' + supw_tmp.name,)
133159

134-
cmd = (initdb, '-D', self.data_directory) + verbose + tuple(opts) + extra_args
160+
cmd = (initdb, '-D', self.data_directory) + tuple(opts) + supw_file + extra_args
135161
p = sp.Popen(
136162
cmd,
137163
close_fds = True,
@@ -142,6 +168,9 @@ def init(self,
142168
p.stdin.close()
143169

144170
rc = p.wait()
171+
if password is not None:
172+
supw_tmp.close()
173+
145174
if rc != 0:
146175
raise pg_exc.InitDBError(cmd, rc, p.stderr.read())
147176

@@ -168,9 +197,9 @@ def start(self,
168197
"""
169198
if self.running():
170199
return None
171-
cmd = [self.postgres_path, '-D', self.data_directory]
200+
cmd = [self.daemon_path, '-D', self.data_directory]
172201
if settings is not None:
173-
for k,v in settings:
202+
for k,v in dict(settings).items():
174203
cmd.append('--{k}={v}'.format(k=k,v=v))
175204

176205
p = sp.Popen(
@@ -205,7 +234,7 @@ def restart(self, timeout = 10):
205234
self.stop()
206235
self.wait_until_stopped(timeout = timeout)
207236
if not self.running():
208-
raise ClusterError("failed to shutdown cluster")
237+
raise pg_exc.ClusterError("failed to shutdown cluster")
209238
self.start()
210239
self.wait_until_started(timeout = timeout)
211240

@@ -253,7 +282,7 @@ def ready_for_connections(self):
253282
'port',
254283
))
255284
if 'listen_addresses' not in d:
256-
raise ClusterError(
285+
raise pg_exc.ClusterError(
257286
"postmaster pings can only be made to TCP/IP configurations"
258287
)
259288

@@ -288,7 +317,7 @@ def ready_for_connections(self):
288317

289318
def wait_until_started(self,
290319
timeout : "how long to wait before throwing a timeout exception" = 10,
291-
delay : "how long to sleep before re-testing" = 0.1
320+
delay : "how long to sleep before re-testing" = 0.05
292321
):
293322
"""
294323
After the `start` method is used, this can be ran in order to block until
@@ -308,15 +337,15 @@ def wait_until_started(self,
308337

309338
def wait_until_stopped(self,
310339
timeout : "how long to wait before throwing a timeout exception" = 10,
311-
delay : "how long to sleep before re-testing" = 0.1
340+
delay : "how long to sleep before re-testing" = 0.05
312341
):
313342
"""
314343
After the `stop` method is used, this can be ran in order to block until
315344
the cluster is shutdown.
316345
317346
Additionally, catching `ClusterTimeoutError` exceptions would be a
318347
starting point for making decisions about whether or not to issue a kill
319-
to the postgres daemon.
348+
to the daemon.
320349
"""
321350
start = time.time()
322351
while self.running():

postgresql/configfile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'PostgreSQL configuration file parser and editor functions.'
66
import sys
77
import os
8-
import postgresql.strings as pg_str
8+
import postgresql.string as pg_str
99
import postgresql.api as pg_api
1010

1111
quote = "'"
@@ -362,7 +362,7 @@ def pg_dotconf(args):
362362
settings[k] = v
363363

364364
fp = ca[0]
365-
with open(fp, 'r') as fr
365+
with open(fp, 'r') as fr:
366366
lines = alter_config(settings, fr)
367367

368368
if co.stdout or fp == '/dev/stdin':

postgresql/exceptions.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,6 @@ class Exception(Exception):
3838
'Base PostgreSQL exception class'
3939
pass
4040

41-
##
42-
# Exceptions pertinent to cluster initialization and management
43-
##
44-
class ClusterError(Exception):
45-
pass
46-
class InitDBError(ClusterError):
47-
pass
48-
class ClusterNotRunningError(ClusterError):
49-
pass
50-
class ClusterTimeoutError(ClusterError):
51-
pass
52-
5341
##
5442
# Miscellaneous exceptions not tied to an SQL state code.
5543
##
@@ -121,10 +109,25 @@ def raise_exception(self, raise_from = None):
121109
else:
122110
raise self from raise_from
123111

112+
##
113+
# Exceptions pertinent to cluster initialization and management
114+
##
115+
class ClusterError(Error):
116+
source = 'CLUSTER'
117+
class ClusterInitializationError(ClusterError):
118+
pass
119+
class InitDBError(ClusterInitializationError):
120+
"A non-zero result was returned by the initdb command"
121+
class ClusterNotRunningError(ClusterError):
122+
pass
123+
class ClusterTimeoutError(ClusterError):
124+
pass
125+
124126
class InsecurityError(Error):
125127
"""
126128
Error signifying a secure channel to a server cannot be established.
127129
"""
130+
source = 'DRIVER'
128131

129132
class TransactionError(Error):
130133
pass

0 commit comments

Comments
 (0)