Skip to content

Commit 88dd678

Browse files
committed
Added flag to return stdout and stderr buffers to get_stdout. Added documentation and examples for this behaviour. Added tests for returned output. Closes #4
1 parent 607666f commit 88dd678

File tree

2 files changed

+79
-8
lines changed

2 files changed

+79
-8
lines changed

pssh.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def exec_command(self, command, sudo=False, **kwargs):
147147
"""
148148
channel = self.client.get_transport().open_session()
149149
channel.get_pty()
150+
# stdin (unused), stdout, stderr
150151
(_, stdout, stderr) = (channel.makefile('wb'), channel.makefile('rb'),
151152
channel.makefile_stderr('rb'))
152153
if sudo:
@@ -260,6 +261,30 @@ def __init__(self, hosts,
260261
>>> print output
261262
[{'myhost1': {'exit_code': 2}}, {'myhost2': {'exit_code': 2}}]
262263
264+
**Example with returned stdout and stderr buffers**
265+
266+
>>> from pssh import ParallelSSHClient, AuthenticationException,\
267+
UnknownHostException, ConnectionErrorException
268+
>>> try:
269+
>>> ... client = ParallelSSHClient(['myhost1', 'myhost2'])
270+
>>> except (AuthenticationException, UnknownHostException, ConnectionErrorException):
271+
>>> ... return
272+
>>> cmds = client.exec_command('ls -ltrh /tmp/aasdfasdf', sudo = True)
273+
>>> output = [client.get_stdout(cmd, return_buffers=True) for cmd in cmds]
274+
>>> print output
275+
[{'myhost1': {'exit_code': 2},
276+
'stdout' : <generator object <genexpr>,
277+
'stderr' : <generator object <genexpr>},
278+
{'myhost2': {'exit_code': 2}
279+
'stdout' : <generator object <genexpr>,
280+
'stderr' : <generator object <genexpr>},
281+
]
282+
>>> for host_stdout in output:
283+
... for line in host_stdout[host_output.keys()[0]]['stdout']:
284+
... print line
285+
ls: cannot access /tmp/aasdfasdf: No such file or directory
286+
ls: cannot access /tmp/aasdfasdf: No such file or directory
287+
263288
**Example with specified private key**
264289
265290
>>> import paramiko
@@ -335,7 +360,7 @@ def _exec_command(self, host, *args, **kwargs):
335360
port=self.port, pkey=self.pkey)
336361
return self.host_clients[host].exec_command(*args, **kwargs)
337362

338-
def get_stdout(self, greenlet):
363+
def get_stdout(self, greenlet, return_buffers=False):
339364
"""Print stdout from greenlet and return exit code for host
340365
341366
:mod:`pssh.get_stdout` will close the open SSH channel but this does
@@ -345,20 +370,38 @@ def get_stdout(self, greenlet):
345370
will open a new channel which is very fast on already established
346371
connections.
347372
373+
By default, stdout and stderr will be logged via the logger named \
374+
`host_logger` unless return_buffers is set to True in which case \
375+
both buffers are returned along with the exit status.
376+
348377
:param greenlet: Greenlet object containing an \
349378
SSH channel reference, hostname, stdout and stderr buffers
350379
:type greenlet: :mod:`gevent.Greenlet`
351380
381+
:param return_buffers: Flag to turn on returning stdout and stderr \
382+
buffers along with exit code. Defaults to off.
383+
:type return_buffers: bool
384+
352385
:rtype: Dictionary containing ``{host: {'exit_code': exit code}}`` entry \
353386
for example ``{'myhost1': {'exit_code': 0}}``
387+
:rtype: With return_buffers=True: ``{'myhost1': {'exit_code': 0},
388+
'stdout' : <iterable>,
389+
'stderr' : <iterable>}``
354390
"""
355-
channel, host, stdout, stderr = greenlet.get()
356-
for line in stdout:
357-
host_logger.info("[%s]\t%s", host, line.strip(),)
358-
for line in stderr:
359-
host_logger.info("[%s] [err] %s", host, line.strip(),)
391+
channel, host, _stdout, _stderr = greenlet.get()
392+
stdout = (line.strip() for line in _stdout)
393+
stderr = (line.strip() for line in _stderr)
360394
channel.close()
361-
return {host: {'exit_code': channel.recv_exit_status()}}
395+
# import ipdb; ipdb.set_trace()
396+
if not return_buffers:
397+
for line in stdout:
398+
host_logger.info("[%s]\t%s", host, line,)
399+
for line in stderr:
400+
host_logger.info("[%s] [err] %s", host, line,)
401+
return {host: {'exit_code': channel.recv_exit_status(),}}
402+
return {host: {'exit_code': channel.recv_exit_status(),
403+
'stdout' : stdout,
404+
'stderr' : stderr, }}
362405

363406
def copy_file(self, local_file, remote_file):
364407
"""Copy local file to remote file in parallel
@@ -402,7 +445,8 @@ def test():
402445
def test_parallel():
403446
client = ParallelSSHClient(['localhost'])
404447
cmds = client.exec_command('ls -ltrh')
405-
print [client.get_stdout(cmd) for cmd in cmds]
448+
output = [client.get_stdout(cmd, return_buffers=True) for cmd in cmds]
449+
print output
406450
cmds = client.copy_file('../test', 'test_dir/test')
407451
client.pool.join()
408452

tests/test_pssh_client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ def test_pssh_client_exec_command(self):
4444
del client
4545
server.join()
4646

47+
def test_pssh_client_exec_command_get_buffers(self):
48+
server = start_server({ self.fake_cmd : self.fake_resp }, self.listen_socket)
49+
client = ParallelSSHClient(['127.0.0.1'], port=self.listen_port,
50+
pkey=self.user_key)
51+
cmd = client.exec_command(self.fake_cmd)[0]
52+
output = client.get_stdout(cmd, return_buffers=True)
53+
expected_exit_code = 0
54+
expected_stdout = [self.fake_resp]
55+
expected_stderr = []
56+
exit_code = output['127.0.0.1']['exit_code']
57+
stdout = list(output['127.0.0.1']['stdout'])
58+
stderr = list(output['127.0.0.1']['stderr'])
59+
self.assertEqual(expected_exit_code, exit_code,
60+
msg = "Got unexpected exit code - %s, expected %s" % (
61+
exit_code,
62+
expected_exit_code,))
63+
self.assertEqual(expected_stdout, stdout,
64+
msg = "Got unexpected stdout - %s, expected %s" % (
65+
stdout,
66+
expected_stdout,))
67+
self.assertEqual(expected_stderr, stderr,
68+
msg = "Got unexpected stderr - %s, expected %s" % (
69+
stderr,
70+
expected_stderr,))
71+
del client
72+
server.join()
73+
4774
def test_pssh_client_auth_failure(self):
4875
server = start_server({ self.fake_cmd : self.fake_resp },
4976
self.listen_socket, fail_auth=True)

0 commit comments

Comments
 (0)