Skip to content

Commit f33a7f3

Browse files
author
Panos Kittenis
committed
Merge branch 'extra-paramiko-kwargs' of git://github.com/Fizzadar/parallel-ssh into fizzadar
2 parents 1d307ed + 77b3d0d commit f33a7f3

File tree

2 files changed

+50
-32
lines changed

2 files changed

+50
-32
lines changed

pssh.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
DEFAULT_RETRIES = 3
4646

4747
logger = logging.getLogger(__name__)
48-
48+
4949
class 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

tests/test_ssh_client.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
os.path.sep.join([os.path.dirname(__file__), 'test_client_private_key']))
3636

3737
class SSHClientTest(unittest.TestCase):
38-
38+
3939
def setUp(self):
4040
self.fake_cmd = 'fake cmd'
4141
self.fake_resp = 'fake response'
@@ -133,6 +133,13 @@ def test_ssh_client_conn_failure(self):
133133
SSHClient, host, port=self.listen_port,
134134
pkey=self.user_key, num_retries=0)
135135

136+
def test_ssh_client_timeout(self):
137+
"""Test connection timeout error"""
138+
with self.assertRaises(ConnectionErrorException) as cm:
139+
SSHClient('127.0.0.1', port=self.listen_port,
140+
pkey=self.user_key, num_retries=0, timeout=1)
141+
142+
self.assertEqual(cm.exception.args[1], 'timed out')
136143

137144
if __name__ == '__main__':
138145
unittest.main()

0 commit comments

Comments
 (0)