Skip to content

Commit f2f3bae

Browse files
author
James William Pye
committed
Implement cluster control for win32.
This allows the tests to be executed on MSW operating systems. Some test exceptions had to be made for test_connect as IPv6 is not turned on by default on the windows builds, and there doesn't appear to be a consistent way to detect that it's on. - Add minor tests for postgresql.installation. - Add a python.os module for providing routines for finding executables and properly extending exe names on win32. - Remove the _e_factors from Installation objects as they do not necessarily come from pg_config binaries or files. Some cleanup work is still needed, so minor refactoring will occur around this area. closes #4
1 parent 56f69a9 commit f2f3bae

16 files changed

Lines changed: 560 additions & 235 deletions

postgresql/cluster.py

Lines changed: 143 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,64 @@
33
# http://python.projects.postgresql.org
44
##
55
"""
6-
Create and interface with PostgreSQL clusters.
6+
Create, control, and destroy PostgreSQL clusters.
77
8-
Primarily, this means starting and stopping the postgres daemon and modifying
9-
the configuration file.
8+
postgresql.cluster provides a programmer's interface to controlling a PostgreSQL
9+
cluster. It provides direct access to proper signalling interfaces.
1010
"""
1111
import sys
1212
import os
1313
import errno
14-
import signal
1514
import time
1615
import io
1716
import subprocess as sp
18-
import tempfile
17+
from tempfile import NamedTemporaryFile
1918

2019
from . import api as pg_api
2120
from . import configfile
2221
from . import installation as pg_inn
2322
from . import exceptions as pg_exc
2423
from . import driver as pg_driver
24+
from .encodings.aliases import get_python_name
25+
from .python.os import close_fds
26+
27+
if sys.platform in ('win32', 'win64'):
28+
from .port import signal1_msw as signal
29+
pg_kill = signal.kill
30+
def namedtemp(encoding):
31+
return NamedTemporaryFile(delete = False, mode = 'w', encoding=encoding)
32+
else:
33+
import signal
34+
pg_kill = os.kill
35+
def namedtemp(encoding):
36+
return NamedTemporaryFile(mode = 'w', encoding=encoding)
37+
38+
class ClusterError(pg_exc.Error):
39+
"""
40+
General cluster error.
41+
"""
42+
code = '-C000'
43+
source = 'CLUSTER'
44+
class ClusterInitializationError(ClusterError):
45+
"General cluster initialization failure"
46+
code = '-Cini'
47+
class InitDBError(ClusterInitializationError):
48+
"A non-zero result was returned by the initdb command"
49+
code = '-Cidb'
50+
class ClusterStartupError(ClusterError):
51+
"Cluster startup failed"
52+
code = '-Cbot'
53+
class ClusterNotRunningError(ClusterError):
54+
"Cluster is not running"
55+
code = '-Cdwn'
56+
class ClusterTimeoutError(ClusterError):
57+
"Cluster operation timed out"
58+
code = '-Cout'
59+
60+
class ClusterWarning(pg_exc.Warning):
61+
"Warning issued by cluster operations"
62+
code = '-Cwrn'
63+
source = 'CLUSTER'
2564

2665
DEFAULT_CLUSTER_ENCODING = 'utf-8'
2766
DEFAULT_CONFIG_FILENAME = 'postgresql.conf'
@@ -38,10 +77,10 @@
3877
'time' : '--lc-time',
3978
'authentication' : '-A',
4079
'user' : '-U',
80+
# pwprompt is not supported.
81+
# Cluster.init is *not* intended for interactive use.
4182
}
4283

43-
pg_kill = os.kill
44-
4584
class Cluster(pg_api.Cluster):
4685
"""
4786
Interface to a PostgreSQL cluster.
@@ -77,6 +116,9 @@ def _e_metas(self):
77116

78117
@property
79118
def daemon_path(self):
119+
"""
120+
Path to the executable to use to startup the cluster.
121+
"""
80122
return self.installation.postmaster or self.installation.postgres
81123

82124
def get_pid_from_file(self):
@@ -110,57 +152,55 @@ def settings(self):
110152
return self._settings
111153

112154
@property
113-
def hba_file(self):
155+
def hba_file(self, join = os.path.join):
156+
"""
157+
The path to the HBA file of the cluster.
158+
"""
114159
return self.settings.get(
115160
'hba_file',
116-
os.path.join(self.data_directory, self.DEFAULT_HBA_FILENAME)
161+
join(self.data_directory, self.DEFAULT_HBA_FILENAME)
117162
)
118163

119-
@classmethod
120-
def from_pg_config_path(type, data_directory, pg_config_path):
121-
"""
122-
Create the cluster using the data_directory and the *path* to pg_config
123-
"""
124-
return type(data_directory, pg_inn.Installation(pg_config_path))
125-
126164
def __init__(self,
165+
installation : "installation object",
127166
data_directory : "path to the data directory",
128-
installation : pg_inn.Installation,
129167
):
130-
self.data_directory = os.path.abspath(data_directory)
131168
self.installation = installation
169+
self.data_directory = os.path.abspath(data_directory)
132170
self.pgsql_dot_conf = os.path.join(
133171
self.data_directory,
134172
self.DEFAULT_CONFIG_FILENAME
135173
)
136174
self.daemon_process = None
137175
self.daemon_command = None
138176

139-
def __repr__(self):
140-
return "%s.%s(%r, %r)" %(
177+
def __repr__(self, format = "{mod}.{name}({ins!r}, {dir!r})".format):
178+
return format(
141179
type(self).__module__,
142180
type(self).__name__,
143-
self.data_directory,
144181
self.installation,
182+
self.data_directory,
145183
)
146184

147-
def __context__(self):
148-
return self
149-
150185
def __enter__(self):
186+
"""
187+
Start the cluster and wait for it to startup.
188+
"""
151189
self.start()
152190
self.wait_until_started()
191+
return self
153192

154193
def __exit__(self, typ, val, tb):
194+
"""
195+
Stop the cluster and wait for it to shutdown.
196+
"""
155197
self.stop()
156198
self.wait_until_stopped()
157-
return typ is None
158199

159200
def init(self,
160201
password : \
161202
"Password to assign to the " \
162203
"cluster's superuser(`user` keyword)." = None,
163-
initdb : "[BEWARE] explicitly state the initdb binary to use" = None,
164204
**kw
165205
):
166206
"""
@@ -170,16 +210,24 @@ def init(self,
170210
`command_option_map` provides the mapping of keyword arguments
171211
to command options.
172212
"""
213+
initdb = self.installation.initdb
173214
if initdb is None:
174-
initdb = self.installation.initdb
175-
if initdb is None:
176-
raise pg_exc.ClusterInitializationError(
177-
"unable to find `initdb` executable for installation: " + \
178-
repr(self.installation),
179-
creator = self
180-
)
215+
initdb = (self.installation.pg_ctl, 'initdb',)
216+
else:
217+
initdb = (initdb,)
181218

219+
if None in initdb:
220+
raise ClusterInitializationError(
221+
"unable to find executable for cluster initialization",
222+
details = {
223+
'detail' : "The installation had neither 'initdb' nor 'pg_ctl'had neither 'initdb' nor 'pg_ctl'",
224+
},
225+
creator = self
226+
)
182227
# Transform keyword options into command options for the executable.
228+
229+
# A default is used rather than looking at the environment to, well,
230+
# avoid looking at the environment.
183231
kw.setdefault('encoding', self.DEFAULT_CLUSTER_ENCODING)
184232
opts = []
185233
for x in kw:
@@ -189,43 +237,53 @@ def init(self,
189237
raise TypeError("got an unexpected keyword argument %r" %(x,))
190238
opts.append(initdb_option_map[x])
191239
opts.append(kw[x])
192-
logfile = kw.get('logfile', sp.PIPE)
240+
logfile = kw.get('logfile') or sp.PIPE
193241
extra_args = tuple([
194242
str(x) for x in kw.get('extra_arguments', ())
195243
])
196244

197245
supw_file = ()
198-
if password is not None:
199-
# got a superuserpass, store it in a tempfile for initdb
200-
supw_tmp = tempfile.NamedTemporaryFile(
201-
mode = 'w', encoding = kw['encoding']
246+
supw_tmp = None
247+
try:
248+
if password is not None:
249+
# got a superuserpass, store it in a tempfile for initdb
250+
supw_tmp = namedtemp(encoding = get_python_name(kw['encoding']))
251+
supw_tmp.write(password)
252+
supw_tmp.flush()
253+
supw_file = ('--pwfile=' + supw_tmp.name,)
254+
255+
cmd = initdb + ('-D', self.data_directory) \
256+
+ tuple(opts) \
257+
+ supw_file \
258+
+ extra_args
259+
260+
p = sp.Popen(
261+
cmd,
262+
close_fds = close_fds,
263+
bufsize = 1024 * 5, # not expecting this to ever be filled.
264+
stdin = sp.PIPE,
265+
stdout = logfile,
266+
# stderr is used to identify a reasonable error message.
267+
stderr = sp.PIPE,
202268
)
203-
supw_tmp.write(password)
204-
supw_tmp.flush()
205-
supw_file = ('--pwfile=' + supw_tmp.name,)
206-
207-
cmd = (initdb, '-D', self.data_directory) \
208-
+ tuple(opts) \
209-
+ supw_file \
210-
+ extra_args
211-
212-
p = sp.Popen(
213-
cmd,
214-
stdin = sp.PIPE,
215-
stdout = logfile,
216-
stderr = sp.PIPE,
217-
)
218-
p.stdin.close()
269+
# stdin is not used; it is not desirable for initdb to be attached.
270+
p.stdin.close()
219271

220-
while True:
221-
try:
222-
rc = p.wait()
223-
break
224-
except OSError as e:
225-
if e.errno != errno.EINTR:
226-
raise
227-
if password is not None:
228-
supw_tmp.close()
272+
while True:
273+
try:
274+
rc = p.wait()
275+
break
276+
except OSError as e:
277+
if e.errno != errno.EINTR:
278+
raise
279+
finally:
280+
# stdlib fail. Make sure the temp gets deleted.
281+
# NamedTemporaryFile has inconsistencies across platforms. :(
282+
if supw_tmp is not None:
283+
n = supw_tmp.name
284+
supw_tmp.close()
285+
if os.path.exists(n):
286+
os.unlink(n)
229287

230288
if rc != 0:
231289
r = p.stderr.read().strip()
@@ -236,11 +294,12 @@ def init(self,
236294
msg = os.linesep.join([
237295
repr(x)[2:-1] for x in r.splitlines()
238296
])
239-
raise pg_exc.InitDBError(
240-
msg,
297+
raise InitDBError(
298+
"initdb exited with non-zero status",
241299
details = {
242-
'COMMAND': cmd,
243-
'RESULT': rc,
300+
'command': cmd,
301+
'stderr': msg,
302+
'stdout': msg,
244303
},
245304
creator = self
246305
)
@@ -253,12 +312,12 @@ def drop(self):
253312
self.shutdown()
254313
try:
255314
self.wait_until_stopped()
256-
except pg_exc.ClusterTimeoutError:
315+
except ClusterTimeoutError:
257316
self.kill()
258317
try:
259318
self.wait_until_stopped()
260-
except pg_exc.ClusterTimeoutError:
261-
pg_exc.ClusterWarning(
319+
except ClusterTimeoutError:
320+
ClusterWarning(
262321
'cluster failed to shutdown after kill',
263322
details = {
264323
'hint' : 'Shared memory may be leaked.'
@@ -289,6 +348,9 @@ def start(self,
289348

290349
p = sp.Popen(
291350
cmd,
351+
close_fds = close_fds,
352+
bufsize = 1024,
353+
# send everything to logfile
292354
stdout = sp.PIPE if logfile is None else logfile,
293355
stderr = sp.STDOUT,
294356
stdin = sp.PIPE,
@@ -304,13 +366,16 @@ def restart(self, logfile = None, settings = None, timeout = 10):
304366
Restart the cluster gracefully.
305367
306368
This provides a higher level interface to stopping then starting the
307-
cluster. It will
369+
cluster. It will perform the wait operations and block until the
370+
restart is complete.
371+
372+
If waiting is not desired, .start() and .stop() should be used directly.
308373
"""
309374
if self.running():
310375
self.stop()
311376
self.wait_until_stopped(timeout = timeout)
312377
if self.running():
313-
raise pg_exc.ClusterError(
378+
raise ClusterError(
314379
"failed to shutdown cluster",
315380
creator = self
316381
)
@@ -524,17 +589,17 @@ def wait_until_started(self,
524589
if self.daemon_process is not None:
525590
r = self.daemon_process.returncode
526591
if r is not None and r != 0:
527-
raise pg_exc.ClusterStartupError(
528-
"postgresql daemon exited with non-zero status",
592+
raise ClusterStartupError(
593+
"postgres daemon exited with non-zero status",
529594
details = {
530595
'RESULT' : r,
531596
'COMMAND' : self.daemon_command,
532597
},
533598
creator = self
534599
)
535600
else:
536-
raise pg_exc.ClusterNotRunningError(
537-
"postgresql daemon has not been started",
601+
raise ClusterNotRunningError(
602+
"postgres daemon has not been started",
538603
creator = self
539604
)
540605
r = self.ready_for_connections()
@@ -549,7 +614,7 @@ def wait_until_started(self,
549614
# condition, rather it's *still* starting up.
550615
if r is not None and isinstance(r, pg_exc.ServerNotReadyError):
551616
raise r
552-
e = pg_exc.ClusterTimeoutError(
617+
e = ClusterTimeoutError(
553618
'timeout on startup',
554619
creator = self
555620
)
@@ -576,7 +641,7 @@ def wait_until_stopped(self,
576641
if self.daemon_process is not None:
577642
self.last_exit_code = self.daemon_process.poll()
578643
if time.time() - start >= timeout:
579-
raise pg_exc.ClusterTimeoutError(
644+
raise ClusterTimeoutError(
580645
'timeout on shutdown',
581646
creator = self,
582647
)

0 commit comments

Comments
 (0)