Skip to content

Commit 1e40f2d

Browse files
author
Dan
committed
Added per-host command arguments and tests. Resolves #43
1 parent b752cd0 commit 1e40f2d

File tree

4 files changed

+151
-22
lines changed

4 files changed

+151
-22
lines changed

.travis.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ script:
1515
- python setup.py nosetests --with-coverage --cover-package=pssh
1616
# If using py3, run 2to3 on embedded server and tests and run nosetests for new test dir
1717
- python -c 'import sys; sys.version_info >= (3,) and sys.exit(1)' || eval "2to3 -nw embedded_server/*.py && 2to3 tests/*.py -o tests3 -nw && cp tests/test_client_private_key* tests3/ && python setup.py nosetests -w tests3 --with-coverage --cover-package=pssh"
18-
notifications:
19-
email:
20-
on_failure: change
2118
after_success:
2219
- coveralls
2320
deploy:

pssh/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ class AuthenticationException(Exception):
3737
class SSHException(Exception):
3838
"""Raised on SSHException error - error authenticating with SSH server"""
3939
pass
40+
41+
42+
class HostArgumentException(Exception):
43+
"""Raised on errors with per-host command arguments"""
44+
pass

pssh/pssh_client.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import warnings
3131
import string
3232
import random
33+
34+
from .exceptions import HostArgumentException
3335
from .constants import DEFAULT_RETRIES
3436
from .ssh_client import SSHClient
3537

@@ -348,13 +350,21 @@ def run_command(self, *args, **kwargs):
348350
:param use_shell: (Optional) Run command with or without shell. Defaults \
349351
to True - use shell defined in user login to run command string
350352
:type use_shell: bool
353+
:param host_args: (Optional) Format command string with per-host \
354+
arguments in ``host_args``. ``host_args`` length must equal length of \
355+
host list - :mod:`pssh.exceptions.HostArgumentException` is raised \
356+
otherwise
357+
:type host_args: tuple or list
351358
:rtype: Dictionary with host as key as per \
352359
:mod:`pssh.pssh_client.ParallelSSHClient.get_output`
353360
354361
:raises: :mod:`pssh.exceptions.AuthenticationException` on authentication error
355362
:raises: :mod:`pssh.exceptions.UnknownHostException` on DNS resolution error
356363
:raises: :mod:`pssh.exceptions.ConnectionErrorException` on error connecting
357364
:raises: :mod:`pssh.exceptions.SSHException` on other undefined SSH errors
365+
:raises: :mod:`pssh.exceptions.HostArgumentException` on number of host \
366+
arguments not equal to number of hosts
367+
:raises: `TypeError` on not enough host arguments for cmd string format
358368
359369
**Example Usage**
360370
@@ -364,14 +374,15 @@ def run_command(self, *args, **kwargs):
364374
365375
output = client.run_command('ls -ltrh')
366376
367-
*print stdout for each command*
377+
**Print stdout for each command**
368378
369379
.. code-block:: python
370380
371381
for host in output:
372-
for line in output[host]['stdout']: print(line)
382+
for line in output[host]['stdout']:
383+
print(line)
373384
374-
*Get exit codes after command has finished*
385+
**Get exit codes after command has finished**
375386
376387
.. code-block:: python
377388
@@ -391,7 +402,8 @@ def run_command(self, *args, **kwargs):
391402
for line in output[host]['stdout']:
392403
print(line)
393404
394-
*Run with sudo*
405+
**Run with sudo**
406+
395407
396408
.. code-block:: python
397409
@@ -410,8 +422,45 @@ def run_command(self, *args, **kwargs):
410422
for host in output:
411423
stdout = list(output[host]['stdout'])
412424
print("Complete stdout for host %s is %s" % (host, stdout,))
425+
426+
**Command with per-host arguments**
427+
428+
``host_args`` keyword parameter can be used to provide string formatting
429+
arguments to use to format the command string.
430+
431+
``host_args`` should be at least as many as number of hosts.
432+
433+
Any string format specification characters may be used in command string.
434+
435+
*Examples*
436+
437+
.. code-block:: python
438+
439+
# Tuple
440+
#
441+
# First host in hosts list will use cmd 'host1_cmd',
442+
# second host 'host2_cmd' and so on
443+
output = client.run_command('%s', host_args=('host1_cmd',
444+
'host2_cmd',
445+
'host3_cmd',))
446+
447+
# Multiple arguments
448+
#
449+
output = client.run_command('%s %s',
450+
host_args=(('host1_cmd1', 'host1_cmd2'),
451+
('host2_cmd1', 'host2_cmd2'),
452+
('host3_cmd1', 'host3_cmd2'),))
453+
454+
# List of dict
455+
#
456+
# Fist host in host list will use cmd 'host-index-0',
457+
# second host 'host-index-1' and so on
458+
output = client.run_command(
459+
'%(cmd)s', host_args=[{'cmd': 'host-index-%s' % (i,))
460+
for i in range(len(client.hosts))]
413461
414462
**Expression as host list**
463+
415464
416465
Any type of iterator may be used as host list, including generator and
417466
list comprehension expressions.
@@ -477,7 +526,7 @@ def run_command(self, *args, **kwargs):
477526
Started 10 commands in 0:00:00.428629
478527
All commands finished in 0:00:05.014757
479528
480-
**Output dictionary**
529+
*Output dictionary*
481530
482531
::
483532
@@ -522,8 +571,20 @@ def run_command(self, *args, **kwargs):
522571
523572
"""
524573
stop_on_errors = kwargs.pop('stop_on_errors', True)
525-
cmds = [self.pool.spawn(self._exec_command, host, *args, **kwargs)
526-
for host in self.hosts]
574+
host_args = kwargs.pop('host_args', None)
575+
if host_args:
576+
try:
577+
cmds = [self.pool.spawn(self._exec_command, host,
578+
args[0] % host_args[host_i],
579+
*args[1:], **kwargs)
580+
for host_i, host in enumerate(self.hosts)]
581+
except IndexError:
582+
raise HostArgumentException(
583+
"Number of host arguments provided does not match "
584+
"number of hosts ")
585+
else:
586+
cmds = [self.pool.spawn(self._exec_command, host, *args, **kwargs)
587+
for host in self.hosts]
527588
output = {}
528589
for cmd in cmds:
529590
try:

tests/test_pssh_client.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,28 @@
1919

2020
"""Unittests for :mod:`pssh.ParallelSSHClient` class"""
2121

22+
2223
import unittest
23-
from pssh import ParallelSSHClient, UnknownHostException, \
24-
AuthenticationException, ConnectionErrorException, SSHException, logger as pssh_logger
25-
from pssh.utils import load_private_key
26-
from embedded_server.embedded_server import start_server, make_socket, \
27-
logger as server_logger, paramiko_logger
28-
from embedded_server.fake_agent import FakeAgent
2924
import random
3025
import logging
31-
import gevent
32-
import paramiko
3326
import os
3427
import warnings
3528
import shutil
3629
import sys
3730

31+
import gevent
32+
from pssh import ParallelSSHClient, UnknownHostException, \
33+
AuthenticationException, ConnectionErrorException, SSHException, \
34+
logger as pssh_logger
35+
from pssh.exceptions import HostArgumentException
36+
from pssh.utils import load_private_key
37+
from embedded_server.embedded_server import start_server, make_socket, \
38+
logger as server_logger, paramiko_logger
39+
from embedded_server.fake_agent import FakeAgent
40+
from paramiko import RSAKey
3841

3942
PKEY_FILENAME = os.path.sep.join([os.path.dirname(__file__), 'test_client_private_key'])
40-
USER_KEY = paramiko.RSAKey.from_private_key_file(PKEY_FILENAME)
43+
USER_KEY = RSAKey.from_private_key_file(PKEY_FILENAME)
4144

4245
server_logger.setLevel(logging.DEBUG)
4346
pssh_logger.setLevel(logging.DEBUG)
@@ -228,7 +231,7 @@ def test_pssh_client_ssh_exception(self):
228231
client = ParallelSSHClient([self.host],
229232
user='fakey', password='fakey',
230233
port=listen_port,
231-
pkey=paramiko.RSAKey.generate(1024),
234+
pkey=RSAKey.generate(1024),
232235
)
233236
self.assertRaises(SSHException, client.run_command, self.fake_cmd)
234237
del client
@@ -710,7 +713,7 @@ def test_ssh_exception(self):
710713
hosts = [host]
711714
client = ParallelSSHClient(hosts, port=port,
712715
user='fakey', password='fakey',
713-
pkey=paramiko.RSAKey.generate(1024))
716+
pkey=RSAKey.generate(1024))
714717
output = client.run_command(self.fake_cmd, stop_on_errors=False)
715718
gevent.sleep(1)
716719
client.pool.join()
@@ -826,7 +829,6 @@ def test_pssh_client_override_allow_agent_authentication(self):
826829
expected_exit_code = 0
827830
expected_stdout = [self.fake_resp]
828831
expected_stderr = []
829-
830832
stdout = list(output[self.host]['stdout'])
831833
stderr = list(output[self.host]['stderr'])
832834
exit_code = output[self.host]['exit_code']
@@ -848,5 +850,69 @@ def test_get_exit_codes_bad_output(self):
848850
self.assertFalse(self.client.get_exit_codes({}))
849851
self.assertFalse(self.client.get_exit_code({}))
850852

853+
def test_per_host_tuple_args(self):
854+
server2_socket = make_socket('127.0.0.2', port=self.listen_port)
855+
server2_port = server2_socket.getsockname()[1]
856+
server2 = start_server(server2_socket)
857+
server3_socket = make_socket('127.0.0.3', port=self.listen_port)
858+
server3_port = server3_socket.getsockname()[1]
859+
server3 = start_server(server3_socket)
860+
hosts = [self.host, '127.0.0.2', '127.0.0.3']
861+
host_args = ('arg1', 'arg2', 'arg3')
862+
cmd = 'echo %s'
863+
client = ParallelSSHClient(hosts, port=self.listen_port,
864+
pkey=self.user_key)
865+
output = client.run_command(cmd, host_args=host_args)
866+
for i, host in enumerate(hosts):
867+
expected = [host_args[i]]
868+
stdout = list(output[host]['stdout'])
869+
self.assertEqual(expected, stdout)
870+
self.assertTrue(output[host]['exit_code'] == 0)
871+
host_args = (('arg1', 'arg2'), ('arg3', 'arg4'), ('arg5', 'arg6'),)
872+
cmd = 'echo %s %s'
873+
output = client.run_command(cmd, host_args=host_args)
874+
for i, host in enumerate(hosts):
875+
expected = ["%s %s" % host_args[i]]
876+
stdout = list(output[host]['stdout'])
877+
self.assertEqual(expected, stdout)
878+
self.assertTrue(output[host]['exit_code'] == 0)
879+
self.assertRaises(HostArgumentException, client.run_command,
880+
cmd, host_args=[host_args[0]])
881+
882+
def test_per_host_dict_args(self):
883+
server2_socket = make_socket('127.0.0.2', port=self.listen_port)
884+
server2_port = server2_socket.getsockname()[1]
885+
server2 = start_server(server2_socket)
886+
server3_socket = make_socket('127.0.0.3', port=self.listen_port)
887+
server3_port = server3_socket.getsockname()[1]
888+
server3 = start_server(server3_socket)
889+
hosts = [self.host, '127.0.0.2', '127.0.0.3']
890+
hosts_gen = (h for h in hosts)
891+
host_args = [dict(zip(('host_arg1', 'host_arg2',),
892+
('arg1-%s' % (i,), 'arg2-%s' % (i,),)))
893+
for i, _ in enumerate(hosts)]
894+
cmd = 'echo %(host_arg1)s %(host_arg2)s'
895+
client = ParallelSSHClient(hosts, port=self.listen_port,
896+
pkey=self.user_key)
897+
output = client.run_command(cmd, host_args=host_args)
898+
for i, host in enumerate(hosts):
899+
expected = ["%(host_arg1)s %(host_arg2)s" % host_args[i]]
900+
stdout = list(output[host]['stdout'])
901+
self.assertEqual(expected, stdout)
902+
self.assertTrue(output[host]['exit_code'] == 0)
903+
self.assertRaises(HostArgumentException, client.run_command,
904+
cmd, host_args=[host_args[0]])
905+
# Host list generator should work also
906+
client.hosts = hosts_gen
907+
output = client.run_command(cmd, host_args=host_args)
908+
for i, host in enumerate(hosts):
909+
expected = ["%(host_arg1)s %(host_arg2)s" % host_args[i]]
910+
stdout = list(output[host]['stdout'])
911+
self.assertEqual(expected, stdout)
912+
self.assertTrue(output[host]['exit_code'] == 0)
913+
client.hosts = (h for h in hosts)
914+
self.assertRaises(HostArgumentException, client.run_command,
915+
cmd, host_args=[host_args[0]])
916+
851917
if __name__ == '__main__':
852918
unittest.main()

0 commit comments

Comments
 (0)