Skip to content

Commit 7b509a4

Browse files
author
Dan
committed
Updated documentation. Added parallel commands example script. Added test for overriding hosts list and using iterator as hosts list.
1 parent ba7c403 commit 7b509a4

File tree

3 files changed

+130
-43
lines changed

3 files changed

+130
-43
lines changed

examples/parallel_commands.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from pssh import ParallelSSHClient
2+
import datetime
3+
4+
output = []
5+
host = 'localhost'
6+
hosts = [host]
7+
client = ParallelSSHClient(hosts)
8+
9+
# Run 10 five second sleeps
10+
cmds = ['sleep 5' for _ in xrange(10)]
11+
start = datetime.datetime.now()
12+
for cmd in cmds:
13+
output.append(client.run_command(cmd, stop_on_errors=False))
14+
end = datetime.datetime.now()
15+
print "Started %s commands on %s host(s) in %s" % (
16+
len(cmds), len(hosts), end-start,)
17+
start = datetime.datetime.now()
18+
for _output in output:
19+
for line in _output[host]['stdout']:
20+
print line
21+
end = datetime.datetime.now()
22+
print "All commands finished in %s" % (end-start,)

pssh/pssh_client.py

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,11 @@ def __init__(self, hosts,
177177
>>> import paramiko
178178
>>> client_key = paramiko.RSAKey.from_private_key_file('user.key')
179179
>>> client = ParallelSSHClient(['myhost1', 'myhost2'], pkey=client_key)
180-
181-
**Example with expression as host list**
182180
183-
Any type of iterator may be used as host list, including generator and
184-
list comprehension expressions.
181+
**Multiple commands**
185182
186-
>>> hosts = ['dc1.myhost1', 'dc2.myhost2']
187-
>>> client = ParallelSSHClient([h for h in hosts if h.find('dc1')])
188-
>>> client.run_command(<..>)
189-
190-
**Overriding host list**
191-
192-
>>> client.hosts = ['otherhost']
193-
>>> print client.run_command('exit 0')
194-
>>> {'otherhost': {'exit_code':0}, <..>}
183+
>>> for cmd in ['uname', 'whoami']:
184+
... client.run_command(cmd)
195185
196186
.. note ::
197187
@@ -225,7 +215,7 @@ def __init__(self, hosts,
225215
self.timeout = timeout
226216
self.proxy_host, self.proxy_port = proxy_host, proxy_port
227217
# To hold host clients
228-
self.host_clients = dict((host, None) for host in hosts)
218+
self.host_clients = {}
229219
self.agent = agent
230220

231221
def run_command(self, *args, **kwargs):
@@ -259,9 +249,9 @@ def run_command(self, *args, **kwargs):
259249
:raises: :mod:`pssh.exceptions.UnknownHostException` on DNS resolution error
260250
:raises: :mod:`pssh.exceptions.ConnectionErrorException` on error connecting
261251
:raises: :mod:`pssh.exceptions.SSHException` on other undefined SSH errors
262-
252+
263253
**Example Usage**
264-
254+
265255
**Simple run command**
266256
267257
>>> output = client.run_command('ls -ltrh')
@@ -291,41 +281,79 @@ def run_command(self, *args, **kwargs):
291281
292282
Capture stdout - **WARNING** - this will store the entirety of stdout
293283
into memory and may exhaust available memory if command output is
294-
large enough:
284+
large enough.
285+
286+
Iterating over stdout/stderr by definition implies blocking until
287+
command has finished. To only see output as it comes in without blocking
288+
the host logger can be enabled - see `Enabling Host Logger` above.
295289
296290
>>> for host in output:
297291
>>> stdout = list(output[host]['stdout'])
298292
>>> print "Complete stdout for host %s is %s" % (host, stdout,)
299-
293+
294+
**Expression as host list**
295+
296+
Any type of iterator may be used as host list, including generator and
297+
list comprehension expressions.
298+
299+
>>> hosts = ['dc1.myhost1', 'dc2.myhost2']
300+
# List comprehension
301+
>>> client = ParallelSSHClient([h for h in hosts if h.find('dc1')])
302+
# Generator
303+
>>> client = ParallelSSHClient((h for h in hosts if h.find('dc1')))
304+
# Filter
305+
>>> client = ParallelSSHClient(filter(lambda h: h.find('dc1'), hosts))
306+
>>> client.run_command(<..>)
307+
308+
.. note ::
309+
310+
Since iterators by design only iterate over a sequence once then stop,
311+
`client.hosts` should be re-assigned after each call to `run_command`
312+
when using iterators as target of `client.hosts`.
313+
314+
**Overriding host list**
315+
316+
Host list can be modified in place. Call to `run_command` will create
317+
new connections as necessary and output will only contain output for
318+
hosts command ran on.
319+
320+
>>> client.hosts = ['otherhost']
321+
>>> print client.run_command('exit 0')
322+
>>> {'otherhost': {'exit_code':0}, <..>}
323+
300324
**Run multiple commands in parallel**
301325
302-
This short example demonstrates running long running commands in parallel
303-
and how long it takes for all commands to start, blocking until they
304-
complete and how long it takes for all commands to complete.
305-
306-
See examples directory for complete example script. ::
326+
This short example demonstrates running long running commands in
327+
parallel, how long it takes for all commands to start, blocking until
328+
they complete and how long it takes for all commands to complete.
307329
308-
output = []
330+
See examples directory for complete script. ::
309331
310-
start = datetime.datetime.now()
311-
cmds = ['sleep 5' for _ in xrange(10)]
312-
for cmd in cmds:
313-
output.append(client.run_command(cmd, stop_on_errors=False))
314-
end = datetime.datetime.now()
315-
print "Started %s commands in %s" % (len(cmds), end-start,)
316-
start = datetime.datetime.now()
317-
for _output in output:
318-
for line in _output[host]['stdout']:
319-
print line
320-
end = datetime.datetime.now()
321-
print "All commands finished in %s" % (end-start,)
332+
output = []
333+
host = 'localhost'
334+
335+
# Run 10 five second sleeps
336+
cmds = ['sleep 5' for _ in xrange(10)]
337+
start = datetime.datetime.now()
338+
for cmd in cmds:
339+
output.append(client.run_command(cmd, stop_on_errors=False))
340+
end = datetime.datetime.now()
341+
print "Started %s commands in %s" % (len(cmds), end-start,)
342+
start = datetime.datetime.now()
343+
for _output in output:
344+
for line in _output[host]['stdout']:
345+
print line
346+
end = datetime.datetime.now()
347+
print "All commands finished in %s" % (end-start,)
322348
323349
*Output*
324350
325-
Started 10 commands in 0:00:00.428629
326-
All commands finished in 0:00:05.014757
351+
::
327352
328-
**Example Output**
353+
Started 10 commands in 0:00:00.428629
354+
All commands finished in 0:00:05.014757
355+
356+
**Output dictionary**
329357
330358
::
331359
@@ -575,7 +603,7 @@ def copy_file(self, local_file, remote_file, recurse=False):
575603

576604
def _copy_file(self, host, local_file, remote_file, recurse=False):
577605
"""Make sftp client, copy file"""
578-
if not self.host_clients[host]:
606+
if not host in self.host_clients or not self.host_clients[host]:
579607
self.host_clients[host] = SSHClient(
580608
host, user=self.user, password=self.password,
581609
port=self.port, pkey=self.pkey,

tests/test_pssh_client.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,51 @@ def test_pssh_hosts_more_than_pool_size(self):
419419
stdout = [list(output[k]['stdout']) for k in output]
420420
expected_stdout = [[self.fake_resp], [self.fake_resp]]
421421
self.assertEqual(len(hosts), len(output),
422-
msg="Did not get output from all hosts. Got output for \
423-
%s/%s hosts" % (len(output), len(hosts),))
422+
msg="Did not get output from all hosts. Got output for " \
423+
"%s/%s hosts" % (len(output), len(hosts),))
424424
self.assertEqual(expected_stdout, stdout,
425425
msg="Did not get expected output from all hosts. \
426426
Got %s - expected %s" % (stdout, expected_stdout,))
427427
del client
428428
del server2
429-
429+
430+
def test_pssh_hosts_iterator_hosts_modification(self):
431+
"""Test using iterator as host list and modifying host list in place"""
432+
server2_socket = make_socket('127.0.0.2', port=self.listen_port)
433+
server2_port = server2_socket.getsockname()[1]
434+
server2 = start_server(server2_socket)
435+
server3_socket = make_socket('127.0.0.3', port=self.listen_port)
436+
server3_port = server3_socket.getsockname()[1]
437+
server3 = start_server(server3_socket)
438+
hosts = [self.host, '127.0.0.2']
439+
client = ParallelSSHClient(iter(hosts),
440+
port=self.listen_port,
441+
pkey=self.user_key,
442+
pool_size=1,
443+
)
444+
output = client.run_command(self.fake_cmd)
445+
stdout = [list(output[k]['stdout']) for k in output]
446+
expected_stdout = [[self.fake_resp], [self.fake_resp]]
447+
self.assertEqual(len(hosts), len(output),
448+
msg="Did not get output from all hosts. Got output for " \
449+
"%s/%s hosts" % (len(output), len(hosts),))
450+
# Run again without re-assigning host list, should do nothing
451+
output = client.run_command(self.fake_cmd)
452+
self.assertFalse(hosts[0] in output,
453+
msg="Expected no host output, got %s" % (output,))
454+
self.assertFalse(output,
455+
msg="Expected empty output, got %s" % (output,))
456+
# Re-assigning host list with new hosts should work
457+
hosts = ['127.0.0.2', '127.0.0.3']
458+
client.hosts = iter(hosts)
459+
output = client.run_command(self.fake_cmd)
460+
self.assertEqual(len(hosts), len(output),
461+
msg="Did not get output from all hosts. Got output for " \
462+
"%s/%s hosts" % (len(output), len(hosts),))
463+
self.assertTrue(hosts[1] in output,
464+
msg="Did not get output for new host %s" % (hosts[1],))
465+
del client, server2, server3
466+
430467
def test_ssh_proxy(self):
431468
"""Test connecting to remote destination via SSH proxy
432469
client -> proxy -> destination

0 commit comments

Comments
 (0)