Skip to content

Commit 1f7e9b4

Browse files
author
Dan
committed
Updated readme and docstrings. Added pty and channel timeout tests. Misc cleanups
1 parent f9e7861 commit 1f7e9b4

File tree

8 files changed

+106
-77
lines changed

8 files changed

+106
-77
lines changed

README.rst

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Installation
2929

3030
pip install parallel-ssh
3131

32-
As of version ``0.93.0`` pip version >= ``6.0.0`` is required for Python 2.6 compatibility with newer versions of gevent which have dropped 2.6 support. This limitation will be removed post ``1.0.0`` releases which will deprecate ``2.6`` support.
32+
As of version ``0.93.0`` pip version >= ``6.0.0`` is required for Python 2.6 compatibility with latest versions of gevent which have dropped 2.6 support. This limitation will be removed post ``1.0.0`` releases which will deprecate ``2.6`` support.
3333

3434
To upgrade ``pip`` run the following - use of ``virtualenv`` is recommended so as not to override system provided packages::
3535

@@ -77,18 +77,22 @@ Exit codes become available once stdout/stderr is iterated on or ``client.join(o
7777
0
7878
0
7979

80-
The client's join function can be used to block and wait for all parallel commands to finish *if output is not needed*. ::
80+
The client's ``join`` function can be used to block and wait for all parallel commands to finish::
8181

8282
client.join(output)
8383

84-
Similarly, if only exit codes are needed but not output ::
84+
Similarly, exit codes are available after ``client.join`` is called::
8585

8686
output = client.run_command('exit 0')
8787
# Block and gather exit codes. Output variable is updated in-place
8888
client.join(output)
8989
print(output[client.hosts[0]]['exit_code'])
9090
0
9191

92+
.. note::
93+
94+
In versions prior to ``1.0.0`` only, ``client.join`` would consume standard output.
95+
9296
There is also a built in host logger that can be enabled to log output from remote hosts. The helper function ``pssh.utils.enable_host_logger`` will enable host logging to stdout, for example ::
9397

9498
import pssh.utils
@@ -128,6 +132,30 @@ On the other end of the spectrum, long lived remote commands that generate *no*
128132

129133
Output *generation* is done remotely and has no effect on the event loop until output is gathered - output buffers are iterated on. Only at that point does the event loop need to be held.
130134

135+
********
136+
SFTP/SCP
137+
********
138+
139+
SFTP is supported (SCP version 2) natively, no ``scp`` command required.
140+
141+
For example to copy a local file to remote hosts in parallel::
142+
143+
from pssh import ParallelSSHClient, utils
144+
from gevent import joinall
145+
146+
utils.enable_logger(utils.logger)
147+
hosts = ['myhost1', 'myhost2']
148+
client = ParallelSSHClient(hosts)
149+
greenlets = client.copy_file('../test', 'test_dir/test')
150+
joinall(greenlets, raise_error=True)
151+
152+
Copied local file ../test to remote destination myhost1:test_dir/test
153+
Copied local file ../test to remote destination myhost2:test_dir/test
154+
155+
There is similar capability to copy remote files to local ones suffixed with the host's name with the ``copy_remote_file`` function.
156+
157+
Directory recursion is supported in both cases via the ``recurse`` parameter - defaults to off.
158+
131159
**************************
132160
Frequently asked questions
133161
**************************
@@ -138,15 +166,15 @@ Frequently asked questions
138166
:A:
139167
In short, the tools are intended for different use cases.
140168

141-
``ParallelSSH`` satisfies uses cases for a parallel SSH client library that scales well over hundreds to hundreds of thousands of hosts - per `Design And Goals`_ - a use case that is very common on cloud platforms and virtual machine automation . It would be best used where it is a good fit for the use case.
169+
``ParallelSSH`` satisfies uses cases for a parallel SSH client library that scales well over hundreds to hundreds of thousands of hosts - per `Design And Goals`_ - a use case that is very common on cloud platforms and virtual machine automation. It would be best used where it is a good fit for the use case at hand.
142170

143-
Fabric and tools like it on the other hand are not well suited to such use cases, for many reasons, performance and differing design goals in particular. The similarity is only that these tools also make use of SSH to run their commands.
171+
Fabric and tools like it on the other hand are not well suited to such use cases, for many reasons, performance and differing design goals in particular. The similarity is only that these tools also make use of SSH to run commands.
144172

145173
``ParallelSSH`` is in other words well suited to be the SSH client tools like Fabric and Ansible and others use to run their commands rather than a direct replacement for.
146174

147175
By focusing on providing a well defined, lightweight - actual code is a few hundred lines - library, ``ParallelSSH`` is far better suited for *run this command on X number of hosts* tasks for which frameworks like Fabric, Capistrano and others are overkill and unsuprisignly, as it is not what they are for, ill-suited to and do not perform particularly well with.
148176

149-
Fabric and tools like it are high level deployment frameworks - as opposed to general purpose libraries - for building deployment tasks to perform on hosts matching a role with task chaining and a DSL like syntax and are primarily intended for command line use for which the framework is a good fit for - very far removed from an SSH client library.
177+
Fabric and tools like it are high level deployment frameworks - as opposed to general purpose libraries - for building deployment tasks to perform on hosts matching a role with task chaining, a DSL like syntax and are primarily intended for command line use for which the framework is a good fit for - very far removed from an SSH client *library*.
150178

151179
Fabric in particular is a port of `Capistrano <https://github.com/capistrano/capistrano>`_ from Ruby to Python. Its design goals are to provide a faithful port of Capistrano with its `tasks` and `roles` framework to python with interactive command line being the intended usage.
152180

@@ -205,28 +233,3 @@ Frequently asked questions
205233

206234
:A:
207235
There is a public `ParallelSSH Google group <https://groups.google.com/forum/#!forum/parallelssh>`_ setup for this purpose - both posting and viewing are open to the public.
208-
209-
210-
********
211-
SFTP/SCP
212-
********
213-
214-
SFTP is supported (SCP version 2) natively, no ``scp`` command required.
215-
216-
For example to copy a local file to remote hosts in parallel::
217-
218-
from pssh import ParallelSSHClient, utils
219-
from gevent import joinall
220-
221-
utils.enable_logger(utils.logger)
222-
hosts = ['myhost1', 'myhost2']
223-
client = ParallelSSHClient(hosts)
224-
greenlets = client.copy_file('../test', 'test_dir/test')
225-
joinall(greenlets, raise_error=True)
226-
227-
Copied local file ../test to remote destination myhost1:test_dir/test
228-
Copied local file ../test to remote destination myhost2:test_dir/test
229-
230-
There is similar capability to copy remote files to local ones suffixed with the host's name with the ``copy_remote_file`` function.
231-
232-
Directory recursion is supported in both cases - defaults to off.

pssh/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,16 @@
1717
# License along with this library; if not, write to the Free Software
1818
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1919

20-
"""Asynchronous parallel SSH library
20+
"""Asynchronous parallel SSH client library.
2121
22-
parallel-ssh uses asychronous network requests - there is *no* multi-threading nor multi-processing used.
22+
Run SSH commands over many - hundreds/hundreds of thousands - number of servers asynchronously and with minimal system load on the client host.
2323
24-
This is a *requirement* for commands on many (hundreds/thousands/hundreds of thousands) of hosts which would grind a system to a halt simply by having so many processes/threads all wanting to execute if done with multi-threading/processing.
24+
New users should start with :py:func:`pssh.pssh_client.ParallelSSHClient.run_command`
2525
26-
The `libev event loop library <http://software.schmorp.de/pkg/libev.html>`_ is utilised on nix systems. Windows is not supported.
27-
28-
See :mod:`pssh.ParallelSSHClient` and :mod:`pssh.SSHClient` for class documentation.
26+
See also :py:class:`pssh.ParallelSSHClient` and :py:class:mod:`pssh.SSHClient` for class documentation.
2927
"""
3028

29+
import logging
3130
from ._version import get_versions
3231
__version__ = get_versions()['version']
3332
del get_versions
@@ -36,7 +35,6 @@
3635
from .utils import enable_host_logger
3736
from .exceptions import UnknownHostException, \
3837
AuthenticationException, ConnectionErrorException, SSHException
39-
import logging
4038

4139
host_logger = logging.getLogger('pssh.host_logger')
4240
logger = logging.getLogger('pssh')

pssh/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class SSHAgent(paramiko.agent.AgentSSH):
2222
supplying an SSH agent"""
2323

2424
def __init__(self):
25+
paramiko.agent.AgentSSH.__init__(self)
2526
self._conn = None
2627
self.keys = []
2728

pssh/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
# License along with this library; if not, write to the Free Software
1616
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1717

18+
"""Constants definitions for pssh package"""
1819

1920
DEFAULT_RETRIES = 3

pssh/pssh_client.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
del sys.modules['threading']
2424
from gevent import monkey
2525
monkey.patch_all()
26+
import string
27+
import random
2628
import logging
29+
2730
import gevent.pool
2831
import gevent.hub
2932
gevent.hub.Hub.NOT_ERROR = (Exception,)
30-
import warnings
31-
import string
32-
import random
3333

3434
from .exceptions import HostArgumentException
3535
from .constants import DEFAULT_RETRIES
@@ -106,8 +106,10 @@ def __init__(self, hosts,
106106
:param host_config: (Optional) Per-host configuration for cases where \
107107
not all hosts use the same configuration values.
108108
:type host_config: dict
109-
:param channel_timeout: (Optional) Time in seconds before an SSH operation \
110-
times out.
109+
:param channel_timeout: (Optional) Time in seconds before reading from \
110+
an SSH channel times out. For example with channel timeout set to one, \
111+
trying to immediately gather output from a command producing no output \
112+
for more than one second will timeout.
111113
:type channel_timeout: int
112114
:param allow_agent: (Optional) set to False to disable connecting to \
113115
the SSH agent
@@ -156,7 +158,8 @@ def __init__(self, hosts,
156158
from remote commands on hosts as it comes in.
157159
158160
This allows for stdout to be automatically logged without having to
159-
print it serially per host.
161+
print it serially per host. :mod:`pssh.utils.host_logger` is a standard
162+
library logger and may be configured to log to anywhere else.
160163
161164
.. code-block:: python
162165
@@ -185,7 +188,7 @@ def __init__(self, hosts,
185188
* Iterating over stdout/stderr to completion
186189
* Calling ``client.join(output)``
187190
188-
is necessary to cause `parallel-ssh` to wait for commands to finish and
191+
is necessary to cause ``parallel-ssh`` to wait for commands to finish and
189192
be able to gather exit codes.
190193
191194
.. note ::
@@ -211,13 +214,13 @@ def __init__(self, hosts,
211214
212215
which returns ``True`` if command has finished.
213216
214-
Either iterating over stdout/stderr or `client.join(output)` will cause exit
217+
Either iterating over stdout/stderr or ``client.join(output)`` will cause exit
215218
codes to become available in output without explicitly calling `get_exit_codes`.
216219
217220
Use ``client.join(output)`` to block until all commands have finished
218221
and gather exit codes at same time.
219222
220-
However, note that ``client.join(output)`` will consume stdout/stderr.
223+
In versions prior to ``1.0.0`` only, ``client.join`` would consume output.
221224
222225
**Exit code retrieval**
223226
@@ -296,7 +299,7 @@ def __init__(self, hosts,
296299
output = client.run_command('ls -ltrh /tmp/aasdfasdf')
297300
client.join(output)
298301
299-
:netstat: ``tcp 0 0 127.0.0.1:53054 127.0.0.1:22 ESTABLISHED``
302+
:netstat: ``tcp 0 0 127.0.0.1:53054 127.0.0.1:22 ESTABLISHED``
300303
301304
Connection remains active after commands have finished executing. Any \
302305
additional commands will use the same connection.
@@ -362,9 +365,12 @@ def run_command(self, *args, **kwargs):
362365
to True - use shell defined in user login to run command string
363366
:type use_shell: bool
364367
:param use_pty: (Optional) Enable/Disable use of pseudo terminal \
365-
emulation. This is required in vast majority of cases, exception \
366-
being where a shell is not used and/or stdout/stderr/stdin buffers \
367-
are not required. Defaults to ``True``
368+
emulation. Disabling it will prohibit capturing standard input/output. \
369+
This is required in majority of cases, exception being where a shell is \
370+
not used and/or input/output is not required. In particular \
371+
when running a command which deliberately closes input/output pipes, \
372+
such as a daemon process, you may want to disable ``use_pty``. \
373+
Defaults to ``True``
368374
:type use_pty: bool
369375
:param host_args: (Optional) Format command string with per-host \
370376
arguments in ``host_args``. ``host_args`` length must equal length of \
@@ -414,7 +420,7 @@ def run_command(self, *args, **kwargs):
414420
0
415421
0
416422
417-
*Wait for completion, update output with exit codes*
423+
*Wait for completion, print exit codes*
418424
419425
.. code-block:: python
420426
@@ -435,9 +441,10 @@ def run_command(self, *args, **kwargs):
435441
into memory and may exhaust available memory if command output is
436442
large enough.
437443
438-
Iterating over stdout/stderr by definition implies blocking until
439-
command has finished. To only log output as it comes in without blocking
440-
the host logger can be enabled - see `Enabling Host Logger` above.
444+
Iterating over stdout/stderr to completion by definition implies
445+
blocking until command has finished. To only log output as it comes in
446+
without blocking the host logger can be enabled - see
447+
`Enabling Host Logger` above.
441448
442449
.. code-block:: python
443450
@@ -502,13 +509,13 @@ def run_command(self, *args, **kwargs):
502509
503510
Since generators by design only iterate over a sequence once then stop,
504511
`client.hosts` should be re-assigned after each call to `run_command`
505-
when using iterators as target of `client.hosts`.
512+
when using generators as target of `client.hosts`.
506513
507514
**Overriding host list**
508515
509516
Host list can be modified in place. Call to `run_command` will create
510517
new connections as necessary and output will only contain output for
511-
hosts command ran on.
518+
the hosts ``run_command`` executed on.
512519
513520
.. code-block:: python
514521
@@ -781,15 +788,16 @@ def copy_file(self, local_file, remote_file, recurse=False):
781788
This function returns a list of greenlets which can be
782789
`join`-ed on to wait for completion.
783790
784-
:py:func:`gevent.joinall` function may be used to join on all greenlets and
785-
will also raise exceptions if called with ``raise_error=True`` - default
786-
is `False`.
791+
:py:func:`gevent.joinall` function may be used to join on all greenlets
792+
and will also raise exceptions from them if called with
793+
``raise_error=True`` - default is `False`.
787794
788795
Alternatively call `.get` on each greenlet to raise any exceptions from
789796
it.
790797
791-
Exceptions listed here are raised when `.get` is called on each
792-
greenlet, not this function itself.
798+
Exceptions listed here are raised when
799+
``gevent.joinall(<greenlets>, raise_error=True)`` or ``.get`` is called on
800+
each greenlet, not this function itself.
793801
794802
:param local_file: Local filepath to copy to remote host
795803
:type local_file: str
@@ -802,7 +810,7 @@ def copy_file(self, local_file, remote_file, recurse=False):
802810
and recurse is not set
803811
:raises: :py:class:`IOError` on I/O errors writing files
804812
:raises: :py:class:`OSError` on OS errors like permission denied
805-
813+
806814
.. note ::
807815
808816
Remote directories in `remote_file` that do not exist will be

pssh/ssh_client.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@
1818

1919
"""Package containing SSHClient class."""
2020

21-
import sys
21+
import os
22+
import logging
23+
from socket import gaierror as sock_gaierror, error as sock_error
24+
2225
from gevent import sleep
2326
import paramiko
2427
from paramiko.ssh_exception import ChannelException
25-
import os
26-
from socket import gaierror as sock_gaierror, error as sock_error
28+
2729
from .exceptions import UnknownHostException, AuthenticationException, \
2830
ConnectionErrorException, SSHException
2931
from .constants import DEFAULT_RETRIES
3032
from .utils import read_openssh_config
31-
import logging
3233

3334
host_logger = logging.getLogger('pssh.host_logger')
3435
logger = logging.getLogger(__name__)
@@ -143,15 +144,15 @@ def _connect_tunnel(self):
143144
logger.info("Connecting via SSH proxy %s:%s -> %s:%s", self.proxy_host,
144145
self.proxy_port, self.host, self.port,)
145146
try:
146-
proxy_channel = self.proxy_client.get_transport().open_channel(
147-
'direct-tcpip', (self.host, self.port,), ('127.0.0.1', 0))
148-
sleep(0)
149-
return self._connect(self.client, self.host, self.port, sock=proxy_channel)
147+
proxy_channel = self.proxy_client.get_transport().open_channel(
148+
'direct-tcpip', (self.host, self.port,), ('127.0.0.1', 0))
149+
sleep(0)
150+
return self._connect(self.client, self.host, self.port, sock=proxy_channel)
150151
except ChannelException as ex:
151-
error_type = ex.args[1] if len(ex.args) > 1 else ex.args[0]
152-
raise ConnectionErrorException("Error connecting to host '%s:%s' - %s",
153-
self.host, self.port,
154-
str(error_type))
152+
error_type = ex.args[1] if len(ex.args) > 1 else ex.args[0]
153+
raise ConnectionErrorException("Error connecting to host '%s:%s' - %s",
154+
self.host, self.port,
155+
str(error_type))
155156

156157
def _connect(self, client, host, port, sock=None, retries=1,
157158
user=None, password=None, pkey=None):
@@ -243,7 +244,7 @@ def exec_command(self, command, sudo=False, user=None,
243244
stdout, stderr, stdin = channel.makefile('rb'), channel.makefile_stderr('rb'), \
244245
channel.makefile('wb')
245246
for _char in ['\\', '"', '$', '`']:
246-
command = command.replace(_char, '\%s' % (_char,))
247+
command = command.replace(_char, r'\%s' % (_char,))
247248
shell = '$SHELL -c' if not shell else shell
248249
_command = ''
249250
if sudo and not user:

pssh/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020

2121

2222
import logging
23-
import gevent
2423
import os
24+
2525
from paramiko.rsakey import RSAKey
2626
from paramiko.dsskey import DSSKey
2727
from paramiko.ecdsakey import ECDSAKey

0 commit comments

Comments
 (0)