4545DEFAULT_RETRIES = 3
4646
4747logger = logging .getLogger (__name__ )
48-
48+
4949class UnknownHostException (Exception ):
5050 """Raised when a host is unknown (dns failure)"""
5151 pass
@@ -74,10 +74,10 @@ class SSHClient(object):
7474 def __init__ (self , host ,
7575 user = None , password = None , port = None ,
7676 pkey = None , forward_ssh_agent = True ,
77- num_retries = DEFAULT_RETRIES , _agent = None ):
77+ num_retries = DEFAULT_RETRIES , _agent = None , timeout = None ):
7878 """Connect to host honouring any user set configuration in ~/.ssh/config \
7979 or /etc/ssh/ssh_config
80-
80+
8181 :param host: Hostname to connect to
8282 :type host: str
8383 :param user: (Optional) User to login as. Defaults to logged in user or \
@@ -94,6 +94,9 @@ def __init__(self, host,
9494 :param num_retries: (Optional) Number of retries for connection attempts\
9595 before the client gives up. Defaults to 3.
9696 :type num_retries: int
97+ :param timeout: (Optional) Number of seconds to timout connection attempts\
98+ before the client gives up. Defaults to 10.
99+ :type timeout: int
97100 :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding - \
98101 equivalent to `ssh -A` from the `ssh` command line utility. \
99102 Defaults to True if not set.
@@ -140,6 +143,7 @@ def __init__(self, host,
140143 if _agent :
141144 self .client ._agent = _agent
142145 self .num_retries = num_retries
146+ self .timeout = timeout
143147 self ._connect ()
144148
145149 def _connect (self , retries = 1 ):
@@ -148,7 +152,7 @@ def _connect(self, retries=1):
148152 self .client .connect (self .host , username = self .user ,
149153 password = self .password , port = self .port ,
150154 pkey = self .pkey ,
151- sock = self .proxy_command )
155+ sock = self .proxy_command , timeout = self . timeout )
152156 except socket .gaierror , e :
153157 logger .error ("Could not resolve host '%s' - retry %s/%s" ,
154158 self .host , retries , self .num_retries )
@@ -164,8 +168,10 @@ def _connect(self, retries=1):
164168 while retries < self .num_retries :
165169 gevent .sleep (5 )
166170 return self ._connect (retries = retries + 1 )
171+
172+ error_type = e .args [1 ] if len (e .args ) > 1 else e .args [0 ]
167173 raise ConnectionErrorException ("%s for host '%s:%s' - retry %s/%s" ,
168- str (e . args [ 1 ] ), self .host , self .port ,
174+ str (error_type ), self .host , self .port ,
169175 retries , self .num_retries ,)
170176 except paramiko .AuthenticationException , e :
171177 raise AuthenticationException (e )
@@ -229,7 +235,7 @@ def _make_sftp(self):
229235
230236 def mkdir (self , sftp , directory ):
231237 """Make directory via SFTP channel
232-
238+
233239 :param sftp: SFTP client object
234240 :type sftp: :mod:`paramiko.SFTPClient`
235241 :param directory: Remote directory to create
@@ -281,7 +287,7 @@ class ParallelSSHClient(object):
281287
282288 def __init__ (self , hosts ,
283289 user = None , password = None , port = None , pkey = None ,
284- forward_ssh_agent = True , num_retries = DEFAULT_RETRIES ,
290+ forward_ssh_agent = True , num_retries = DEFAULT_RETRIES , timeout = None ,
285291 pool_size = 10 ):
286292 """
287293 :param hosts: Hosts to connect to
@@ -300,18 +306,21 @@ def __init__(self, hosts,
300306 :param num_retries: (Optional) Number of retries for connection attempts\
301307 before the client gives up. Defaults to 3.
302308 :type num_retries: int
309+ :param timeout: (Optional) Number of seconds to timout connection attempts\
310+ before the client gives up. Defaults to 10.
311+ :type timeout: int
303312 :param forward_ssh_agent: (Optional) Turn on SSH agent forwarding - \
304313 equivalent to `ssh -A` from the `ssh` command line utility. \
305314 Defaults to True if not set.
306315 :type forward_ssh_agent: bool
307316 :param pool_size: (Optional) Greenlet pool size. Controls on how many\
308317 hosts to execute tasks in parallel. Defaults to 10
309318 :type pool_size: int
310-
319+
311320 **Example**
312321
313322 >>> from pssh import ParallelSSHClient, AuthenticationException,\
314- UnknownHostException, ConnectionErrorException
323+ UnknownHostException, ConnectionErrorException
315324 >>> client = ParallelSSHClient(['myhost1', 'myhost2'])
316325 >>> try:
317326 >>> ... cmds = client.exec_command('ls -ltrh /tmp/aasdfasdf', sudo = True)
@@ -326,7 +335,7 @@ def __init__(self, hosts,
326335 **Example with returned stdout and stderr buffers**
327336
328337 >>> from pssh import ParallelSSHClient, AuthenticationException,\
329- UnknownHostException, ConnectionErrorException
338+ UnknownHostException, ConnectionErrorException
330339 >>> client = ParallelSSHClient(['myhost1', 'myhost2'])
331340 >>> try:
332341 >>> ... cmds = client.exec_command('ls -ltrh /tmp/aasdfasdf', sudo = True)
@@ -335,10 +344,10 @@ def __init__(self, hosts,
335344 >>> output = [client.get_stdout(cmd, return_buffers=True) for cmd in cmds]
336345 >>> print output
337346 [{'myhost1': {'exit_code': 2,
338- 'stdout' : <generator object <genexpr>,
347+ 'stdout' : <generator object <genexpr>,
339348 'stderr' : <generator object <genexpr>,}},
340349 {'myhost2': {'exit_code': 2,
341- 'stdout' : <generator object <genexpr>,
350+ 'stdout' : <generator object <genexpr>,
342351 'stderr' : <generator object <genexpr>,}},
343352 ]
344353 >>> for host_stdout in output:
@@ -352,25 +361,25 @@ def __init__(self, hosts,
352361 >>> import paramiko
353362 >>> client_key = paramiko.RSAKey.from_private_key_file('user.key')
354363 >>> client = ParallelSSHClient(['myhost1', 'myhost2'], pkey=client_key)
355-
364+
356365 .. note ::
357-
366+
358367 **Connection persistence**
359-
368+
360369 Connections to hosts will remain established for the duration of the
361370 object's life. To close them, just `del` or reuse the object reference.
362-
371+
363372 >>> client = ParallelSSHClient(['localhost'])
364373 >>> cmds = client.exec_command('ls -ltrh /tmp/aasdfasdf')
365374 >>> cmds[0].join()
366-
375+
367376 :netstat: ``tcp 0 0 127.0.0.1:53054 127.0.0.1:22 ESTABLISHED``
368-
377+
369378 Connection remains active after commands have finished executing. Any \
370379 additional commands will use the same connection.
371-
380+
372381 >>> del client
373-
382+
374383 Connection is terminated.
375384 """
376385 self .pool = gevent .pool .Pool (size = pool_size )
@@ -382,6 +391,7 @@ def __init__(self, hosts,
382391 self .port = port
383392 self .pkey = pkey
384393 self .num_retries = num_retries
394+ self .timeout = timeout
385395 # To hold host clients
386396 self .host_clients = dict ((host , None ) for host in hosts )
387397
@@ -396,39 +406,40 @@ def exec_command(self, *args, **kwargs):
396406 :rtype: List of :mod:`gevent.Greenlet`
397407
398408 **Example**:
399-
409+
400410 >>> cmds = client.exec_command('ls -ltrh')
401-
411+
402412 Wait for completion, no stdout:
403-
413+
404414 >>> for cmd in cmds:
405415 >>> cmd.join()
406-
416+
407417 Alternatively/in addition print stdout for each command:
408-
418+
409419 >>> print [get_stdout(cmd) for cmd in cmds]
410420
411421 Retrieving stdout implies join, meaning get_stdout will wait
412422 for completion of all commands before returning output.
413-
423+
414424 You may call get_stdout on already completed greenlets to re-get
415425 their output as many times as you want."""
416426 return [self .pool .spawn (self ._exec_command , host , * args , ** kwargs )
417427 for host in self .hosts ]
418-
428+
419429 def _exec_command (self , host , * args , ** kwargs ):
420430 """Make SSHClient, run command on host"""
421431 if not self .host_clients [host ]:
422432 self .host_clients [host ] = SSHClient (host , user = self .user ,
423433 password = self .password ,
424434 port = self .port , pkey = self .pkey ,
425435 forward_ssh_agent = self .forward_ssh_agent ,
426- num_retries = self .num_retries )
436+ num_retries = self .num_retries ,
437+ timeout = self .timeout )
427438 return self .host_clients [host ].exec_command (* args , ** kwargs )
428439
429440 def get_stdout (self , greenlet , return_buffers = False ):
430441 """Get/print stdout from greenlet and return exit code for host
431-
442+
432443 :mod:`pssh.get_stdout` will close the open SSH channel but this does
433444 **not** close the established connection to the remote host, only the
434445 authenticated SSH channel within it. This is standard practise
@@ -452,7 +463,7 @@ def get_stdout(self, greenlet, return_buffers=False):
452463 for example ``{'myhost1': {'exit_code': 0}}``
453464 :rtype: With ``return_buffers=True``: ``{'myhost1': {'exit_code': 0,
454465 'channel' : None or SSH channel of command if command is still executing,
455- 'stdout' : <iterable>,
466+ 'stdout' : <iterable>,
456467 'stderr' : <iterable>,}}``
457468 """
458469 gevent .sleep (.2 )
@@ -488,7 +499,7 @@ def get_stdout(self, greenlet, return_buffers=False):
488499
489500 def copy_file (self , local_file , remote_file ):
490501 """Copy local file to remote file in parallel
491-
502+
492503 :param local_file: Local filepath to copy to remote host
493504 :type local_file: str
494505 :param remote_file: Remote filepath on remote host to copy file to
0 commit comments