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"""
1111import sys
1212import os
1313import errno
14- import signal
1514import time
1615import io
1716import subprocess as sp
18- import tempfile
17+ from tempfile import NamedTemporaryFile
1918
2019from . import api as pg_api
2120from . import configfile
2221from . import installation as pg_inn
2322from . import exceptions as pg_exc
2423from . 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
2665DEFAULT_CLUSTER_ENCODING = 'utf-8'
2766DEFAULT_CONFIG_FILENAME = 'postgresql.conf'
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-
4584class 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