11# This file is part of parallel-ssh.
22
3- # Copyright (C) 2015 Panos Kittenis
3+ # Copyright (C) 2015- Panos Kittenis
44
55# This library is free software; you can redistribute it and/or
66# modify it under the terms of the GNU Lesser General Public
1616# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1717
1818
19- """Package containing ParallelSSHClient class"""
19+ """Package containing ParallelSSHClient class. """
2020
2121
2222from gevent import monkey
2626import gevent .hub
2727gevent .hub .Hub .NOT_ERROR = (Exception ,)
2828import warnings
29+ import hashlib
2930from .constants import DEFAULT_RETRIES
3031from .ssh_client import SSHClient
3132
3738class ParallelSSHClient (object ):
3839 """Uses :mod:`pssh.ssh_client.SSHClient`, performs tasks over SSH on multiple hosts in \
3940 parallel.
40-
41+
4142 Connections to hosts are established in parallel when ``run_command`` is called,
4243 therefor any connection and/or authentication exceptions will happen on the
43- call to ``run_command`` and need to be caught."""
44-
44+ call to ``run_command`` and need to be caught.
45+ """
46+
4547 def __init__ (self , hosts ,
4648 user = None , password = None , port = None , pkey = None ,
4749 forward_ssh_agent = True , num_retries = DEFAULT_RETRIES , timeout = 120 ,
@@ -84,39 +86,76 @@ def __init__(self, hosts,
8486 set. Defaults to 22.
8587 :type proxy_port: int
8688
87- **Example**
88-
89- >>> from pssh import ParallelSSHClient, AuthenticationException, \
89+ **Example Usage**
90+
91+ >>> from pssh.pssh_client import ParallelSSHClient
92+ >>> from pssh.exceptions import AuthenticationException, \
9093 UnknownHostException, ConnectionErrorException
94+
9195 >>> client = ParallelSSHClient(['myhost1', 'myhost2'])
9296 >>> try:
9397 >>> ... output = client.run_command('ls -ltrh /tmp/aasdfasdf', sudo=True)
9498 >>> except (AuthenticationException, UnknownHostException, ConnectionErrorException):
95- >>> ... return
99+ >>> ... pass
100+
96101 >>> # Commands have started executing at this point
97102 >>> # Exit code will probably not be available immediately
98103 >>> print output
99- >>> {'myhost1': {'exit_code': None,
104+
105+ ::
106+
107+ {'myhost1': {'exit_code': None,
100108 'stdout' : <generator>,
101109 'stderr' : <generator>,
102110 'cmd' : <greenlet>,
111+ 'exception' : None,
103112 },
104113 'myhost2': {'exit_code': None,
105114 'stdout' : <generator>,
106115 'stderr' : <generator>,
107116 'cmd' : <greenlet>,
117+ 'exception' : None,
108118 }}
109- >>> # Print output as it comes in.
119+
120+ **Enabling host logger**
121+
122+ There is a host logger in parallel-ssh that can be enabled to show stdout
123+ *in parallel* from remote commands on hosts as it comes in.
124+
125+ This allows for stdout to be automatically displayed without having to
126+ print it serially per host.
127+
128+ >>> import pssh.utils
129+ >>> pssh.utils.enable_host_logger()
130+ >>> output = client.run_command('ls -ltrh')
131+ [myhost1] drwxrwxr-x 6 user group 4.0K Jan 1 HH:MM x
132+ [myhost2] drwxrwxr-x 6 user group 4.0K Jan 1 HH:MM x
133+
134+ Retrieve exit codes after commands have finished as below. This is
135+ only necessary for long running commands that do not exit immediately.
136+
137+ ``exit_code`` in ``output`` will be ``None`` if command has not finished.
138+
139+ ``get_exit_codes`` is not a blocking function and will not wait for commands
140+ to finish. Use ``client.pool.join()`` to block until all commands have
141+ finished.
142+
143+ ``output`` parameter is modified in-place.
144+
145+ >>> client.get_exit_codes(output)
146+ >>> for host in output:
147+ >>> ... print output[host]['exit_code']
148+ 0
149+ 0
150+
151+ Print stdout serially per host as it becomes available.
152+
110153 >>> for host in output: for line in output[host]['stdout']: print line
111154 [myhost1] ls: cannot access /tmp/aasdfasdf: No such file or directory
112155 [myhost2] ls: cannot access /tmp/aasdfasdf: No such file or directory
113- >>> # Retrieve exit code after commands have finished
114- >>> # `get_exit_code` will return `None` if command has not finished
115- >>> print client.get_exit_code(output[host])
116- 0
117-
156+
118157 **Example with specified private key**
119-
158+
120159 >>> import paramiko
121160 >>> client_key = paramiko.RSAKey.from_private_key_file('user.key')
122161 >>> client = ParallelSSHClient(['myhost1', 'myhost2'], pkey=client_key)
@@ -167,39 +206,45 @@ def run_command(self, *args, **kwargs):
167206 :type args: tuple
168207 :param sudo: (Optional) Run with sudo. Defaults to False
169208 :type sudo: bool
170- :param kwargs: Keyword arguments for command
171- :type kwargs: dict
172209 :param stop_on_errors: (Optional) Raise exception on errors running command. \
173- Defaults to True.
210+ Defaults to True. With stop_on_errors set to False, exceptions are instead \
211+ added to output of `run_command`. See example usage below.
174212 :type stop_on_errors: bool
175- :rtype: Dictionary with host as key as per :mod:`pssh.pssh_client.ParallelSSH.get_output`
213+ :param kwargs: Keyword arguments for command
214+ :type kwargs: dict
215+ :rtype: Dictionary with host as key as per \
216+ :mod:`pssh.pssh_client.ParallelSSHClient.get_output`
176217
177218 :raises: :mod:`pssh.exceptions.AuthenticationException` on authentication error
178219 :raises: :mod:`pssh.exceptions.UnknownHostException` on DNS resolution error
179220 :raises: :mod:`pssh.exceptions.ConnectionErrorException` on error connecting
180221 :raises: :mod:`pssh.exceptions.SSHException` on other undefined SSH errors
181222
182223 **Example Usage**
224+
225+ **Simple run command**
183226
184227 >>> output = client.run_command('ls -ltrh')
185-
186- print stdout for each command:
228+
229+ * print stdout for each command*
187230
188231 >>> for host in output:
189232 >>> for line in output[host]['stdout']: print line
190233
191- Get exit code after command has finished:
234+ * Get exit codes after command has finished*
192235
236+ >>> client.get_exit_codes(output)
193237 >>> for host in output:
194- >>> for line in output[host]['stdout']: print line
195- >>> exit_code = client.get_exit_code(output[host])
238+ >>> ... print output[host]['exit_code']
239+ 0
240+ 0
196241
197- Wait for completion, no stdout:
242+ * Wait for completion, no stdout*
198243
199244 >>> client.pool.join()
200-
201- Run with sudo:
202-
245+
246+ * Run with sudo*
247+
203248 >>> output = client.run_command('ls -ltrh', sudo=True)
204249
205250 Capture stdout - **WARNING** - this will store the entirety of stdout
@@ -209,17 +254,35 @@ def run_command(self, *args, **kwargs):
209254 >>> for host in output:
210255 >>> stdout = list(output[host]['stdout'])
211256 >>> print "Complete stdout for host %s is %s" % (host, stdout,)
212-
257+
213258 **Example Output**
214-
259+
215260 ::
216261
217262 {'myhost1': {'exit_code': exit code if ready else None,
218263 'channel' : SSH channel of command,
219264 'stdout' : <iterable>,
220265 'stderr' : <iterable>,
221- 'cmd' : <greenlet>}}
222-
266+ 'cmd' : <greenlet>},
267+ 'exception' : None}
268+
269+ **Do not stop on errors, return per-host exceptions in output**
270+
271+ >>> output = client.run_command('ls -ltrh', stop_on_errors=False)
272+ >>> client.pool.join()
273+ >>> print output
274+
275+ ::
276+
277+ {'myhost1': {'exit_code': None,
278+ 'channel' : None,
279+ 'stdout' : None,
280+ 'stderr' : None,
281+ 'cmd' : None,
282+ 'exception' : ConnectionErrorException(
283+ "Error connecting to host '%s:%s' - %s - retry %s/%s",
284+ host, port, 'Connection refused', 3, 3)}}
285+
223286 """
224287 stop_on_errors = kwargs .pop ('stop_on_errors' , True )
225288 cmds = [self .pool .spawn (self ._exec_command , host , * args , ** kwargs )
@@ -303,34 +366,43 @@ def get_output(self, cmd, output):
303366 except IndexError :
304367 logger .error ("Got exception with no host argument - cannot update output data with %s" , ex )
305368 raise ex
306- output .setdefault (host , {})
307- output [host ].update ({'exit_code' : None ,
308- 'channel' : None ,
309- 'stdout' : None ,
310- 'stderr' : None ,
311- 'cmd' : cmd ,
312- 'exception' : ex ,})
369+ self ._update_host_output (output , host , None , None , None , None , cmd ,
370+ exception = ex )
313371 raise ex
372+ self ._update_host_output (output , host , self ._get_exit_code (channel ),
373+ channel , stdout , stderr , cmd )
374+
375+ def _update_host_output (self , output , host , exit_code , channel , stdout , stderr , cmd ,
376+ exception = None ):
377+ """Update host output with given data"""
378+ if host in output :
379+ new_host = "_" .join ([host , hashlib .sha1 ().hexdigest ()[:10 ]])
380+ logger .warning ("Already have output for host %s - changing host key for %s to %s" ,
381+ host , host , new_host )
382+ host = new_host
314383 output .setdefault (host , {})
315- output [host ].update ({'exit_code' : self . _get_exit_code ( channel ) ,
384+ output [host ].update ({'exit_code' : exit_code ,
316385 'channel' : channel ,
317386 'stdout' : stdout ,
318387 'stderr' : stderr ,
319- 'cmd' : cmd , })
388+ 'cmd' : cmd ,
389+ 'exception' : exception ,})
320390
321391 def get_exit_codes (self , output ):
322- """Get exit code for all hosts in output if available.
392+ """Get exit code for all hosts in output * if available* .
323393 Output parameter is modified in-place.
324394
325- :param output: As returned by `self .get_output`
395+ :param output: As returned by :mod:`pssh.pssh_client.ParallelSSHClient .get_output`
326396 :rtype: None
327397 """
328398 for host in output :
329399 output [host ].update ({'exit_code' : self .get_exit_code (output [host ])})
330400
331401 def get_exit_code (self , host_output ):
332- """Get exit code from host output if available
333- :param host_output: Per host output as returned by `self.get_output`
402+ """Get exit code from host output *if available*.
403+
404+ :param host_output: Per host output as returned by \
405+ :mod:`pssh.pssh_client.ParallelSSHClient.get_output`
334406 :rtype: int or None if exit code not ready"""
335407 if not 'channel' in host_output :
336408 logger .error ("%s does not look like host output.." , host_output ,)
@@ -348,7 +420,7 @@ def _get_exit_code(self, channel):
348420 def get_stdout (self , greenlet , return_buffers = False ):
349421 """Get/print stdout from greenlet and return exit code for host
350422
351- **Deprecated** - use self. get_output() instead.
423+ **Deprecated** - use :mod:`pssh.pssh_client.ParallelSSHClient. get_output` instead.
352424
353425 :param greenlet: Greenlet object containing an \
354426 SSH channel reference, hostname, stdout and stderr buffers
0 commit comments