Skip to content

Commit ab79b3a

Browse files
author
Pan
committed
Cleanups, bug fixes for #104.
Updated documentation, changelog. Added raising timeout on native client wait_finished. Resolves #104.
1 parent f94aaf9 commit ab79b3a

File tree

7 files changed

+119
-17
lines changed

7 files changed

+119
-17
lines changed

Changelog.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
Change Log
22
============
33

4+
1.5.0
5+
++++++
6+
7+
Changes
8+
---------
9+
10+
* ``ParallelSSH2Client.join`` with timeout now consumes output to ensure command completion status is accurate.
11+
* Output reading now raises ``pssh.exceptions.Timeout`` exception when timeout is requested and reached with command still running.
12+
13+
Fixes
14+
------
15+
16+
* ``ParallelSSH2Client.join`` would always raise ``Timeout`` when output has not been consumed even if command has finished - #104.
17+
418
1.4.0
519
++++++
620

doc/advanced.rst

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,63 @@ Configuration for the proxy host's user name, port, password and private key can
139139
140140
Where ``proxy.key`` is a filename containing private key to use for proxy host authentication.
141141

142-
In the above example, connections to the target hosts are made via ``my_proxy_user@bastion:2222`` -> ``target_host_user@<host>``.
142+
In the above example, connections to the target hosts are made via SSH through ``my_proxy_user@bastion:2222`` -> ``target_host_user@<host>``.
143143

144144
.. note::
145145

146146
Proxy host connections are asynchronous and use the SSH protocol's native TCP tunneling - aka local port forward. No external commands or processes are used for the proxy connection, unlike the `ProxyCommand` directive in OpenSSH and other utilities.
147147

148148
While connections initiated by ``parallel-ssh`` are asynchronous, connections from proxy host -> target hosts may not be, depending on SSH server implementation. If only one proxy host is used to connect to a large number of target hosts and proxy SSH server connections are *not* asynchronous, this may adversely impact performance on the proxy host.
149149

150+
Join and Output Timeouts
151+
**************************
152+
153+
*New in 1.5.0*
154+
155+
The native clients have timeout functionality on reading output and ``client.join``.
156+
157+
.. code-block:: python
158+
159+
from pssh.exceptions import Timeout
160+
161+
output = client.run_command(..)
162+
try:
163+
client.join(output, timeout=5)
164+
except Timeout:
165+
pass
166+
167+
.. code-block:: python
168+
169+
output = client.run_command(.., timeout=5)
170+
for host, host_out in output.items():
171+
try:
172+
for line in host_out.stdout:
173+
pass
174+
for line in host_out.stderr:
175+
pass
176+
except Timeout:
177+
pass
178+
179+
The client will raise a ``Timeout`` exception if remote commands have not finished within five seconds in the above examples.
180+
181+
.. note::
182+
183+
``join`` with a timeout forces output to be consumed as otherwise the pending output will keep the channel open and make it appear as if command has not yet finished.
184+
185+
To capture output when using ``join`` with a timeout, gather output first before calling ``join``, making use of output timeout as well, and/or make use of :ref:`host logger` functionality.
186+
187+
188+
.. warning::
189+
190+
Beware of race conditions when using timeout functionality. For best results, only send one command per call to ``run_command`` when using timeout functionality.
191+
192+
As the timeouts are performed on ``select`` calls on the socket which is responsible for all client <-> server communication, whether or not a timeout will occur depends on what the socket is doing at that time.
193+
194+
Multiple commands like ``run_command('echo blah; sleep 5')`` where ``sleep 5`` is a placeholder for something taking five seconds to complete will result in a race condition as the second command may or may not have started by the time ``join`` is called or output is read which will cause timeout to *not* be raised even if the second command has not started or completed.
195+
196+
It is responsibility of developer to avoid these race conditions such as by only sending one command in such cases.
197+
198+
150199
Per-Host Configuration
151200
***********************
152201

doc/quickstart.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ Commands last executed by ``run_command`` can also be retrieved from the ``cmds`
193193
194194
*New in 1.2.0*
195195

196+
.. _host logger:
197+
196198
Host Logger
197199
------------
198200

pssh/pssh2_client.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,29 +204,45 @@ def join(self, output, consume_output=False, timeout=None):
204204
output on call to ``join`` when host logger has been enabled.
205205
:type consume_output: bool
206206
:param timeout: Timeout in seconds if remote command is not yet
207-
finished.
207+
finished. Note that use of timeout forces ``consume_output=True``
208+
otherwise the channel output pending to be consumed always results
209+
in the channel not being finished.
208210
:type timeout: int
209211
210212
:raises: :py:class:`pssh.exceptions.Timeout` on timeout requested and
211213
reached with commands still running.
212214
213215
:rtype: ``None``"""
214-
for host in output:
216+
for host, host_out in output.items():
215217
if host not in self.host_clients or self.host_clients[host] is None:
216218
continue
217-
self.host_clients[host].wait_finished(output[host].channel,
218-
timeout=timeout)
219-
if timeout and not output[host].channel.eof():
219+
client = self.host_clients[host]
220+
channel = host_out.channel
221+
try:
222+
client.wait_finished(channel, timeout=timeout)
223+
except Timeout:
220224
raise Timeout(
221225
"Timeout of %s sec(s) reached on host %s with command "
222226
"still running", timeout, host)
227+
stdout = host_out.stdout
228+
stderr = host_out.stderr
229+
if timeout:
230+
# Must consume buffers prior to EOF check
231+
self._consume_output(stdout, stderr)
232+
if not channel.eof():
233+
raise Timeout(
234+
"Timeout of %s sec(s) reached on host %s with command "
235+
"still running", timeout, host)
223236
elif consume_output:
224-
for line in output[host].stdout:
225-
pass
226-
for line in output[host].stderr:
227-
pass
237+
self._consume_output(stdout, stderr)
228238
self.get_exit_codes(output)
229239

240+
def _consume_output(self, stdout, stderr):
241+
for line in stdout:
242+
pass
243+
for line in stderr:
244+
pass
245+
230246
def _get_exit_code(self, channel):
231247
"""Get exit code from channel if ready"""
232248
if channel is None:

pssh/ssh2_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
LIBSSH2_SFTP_S_IXGRP, LIBSSH2_SFTP_S_IXOTH
3838

3939
from .exceptions import UnknownHostException, AuthenticationException, \
40-
ConnectionErrorException, SessionError, SFTPError, SFTPIOError
40+
ConnectionErrorException, SessionError, SFTPError, SFTPIOError, Timeout
4141
from .constants import DEFAULT_RETRIES, RETRY_DELAY
4242
from .native._ssh2 import wait_select, _read_output # , sftp_get, sftp_put
4343

@@ -282,14 +282,19 @@ def wait_finished(self, channel, timeout=None):
282282
if channel is None:
283283
return
284284
# If .eof() returns EAGAIN after a select with a timeout, it means
285-
# it reached timeout without EOF and the connection should not be
285+
# it reached timeout without EOF and the channel should not be
286286
# closed as the command is still running.
287287
ret = channel.wait_eof()
288288
while ret == LIBSSH2_ERROR_EAGAIN:
289289
wait_select(self.session, timeout=timeout)
290290
ret = channel.wait_eof()
291291
if ret == LIBSSH2_ERROR_EAGAIN and timeout is not None:
292-
return
292+
raise Timeout
293+
# Close channel to indicate no more commands will be sent over it
294+
self.close_channel(channel)
295+
296+
def close_channel(self, channel):
297+
logger.debug("Closing channel")
293298
self._eagain(channel.close)
294299
self._eagain(channel.wait_closed)
295300

tests/test_pssh_ssh2_client.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def setUpClass(cls):
6767
cls.user_key = PKEY_FILENAME
6868
cls.user_pub_key = PUB_FILE
6969
cls.user = pwd.getpwuid(os.geteuid()).pw_name
70+
# Single client for all tests ensures that the client does not do
71+
# anything that causes server to disconnect the session and
72+
# affect all subsequent uses of the same session.
7073
cls.client = ParallelSSHClient([cls.host],
7174
pkey=PKEY_FILENAME,
7275
port=cls.port,
@@ -1166,13 +1169,27 @@ def test_host_no_client(self):
11661169
def test_join_timeout(self):
11671170
client = ParallelSSHClient([self.host], port=self.port,
11681171
pkey=self.user_key)
1169-
output = client.run_command('sleep 2')
1172+
output = client.run_command('echo me; sleep 2')
1173+
# Wait for long running command to start to avoid race condition
1174+
time.sleep(.1)
11701175
self.assertRaises(Timeout, client.join, output, timeout=1)
11711176
self.assertFalse(output[self.host].channel.eof())
1172-
client.join(output, timeout=2)
1177+
# Ensure command has actually finished - avoid race conditions
1178+
time.sleep(2)
1179+
client.join(output, timeout=3)
11731180
self.assertTrue(output[self.host].channel.eof())
11741181
self.assertTrue(client.finished(output))
11751182

1183+
def test_join_timeout_set_no_timeout(self):
1184+
client = ParallelSSHClient([self.host], port=self.port,
1185+
pkey=self.user_key)
1186+
output = client.run_command('echo me; sleep 1')
1187+
# Allow enough time for blocking command to start - avoid race condition
1188+
time.sleep(.1)
1189+
client.join(output, timeout=2)
1190+
self.assertTrue(client.finished(output))
1191+
self.assertTrue(output[self.host].channel.eof())
1192+
11761193
def test_read_timeout(self):
11771194
client = ParallelSSHClient([self.host], port=self.port,
11781195
pkey=self.user_key)

tests/test_ssh2_client.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ def test_long_running_cmd(self):
5151
def test_manual_auth(self):
5252
client = SSHClient(self.host, port=self.port,
5353
pkey=self.user_key,
54-
num_retries=1,
55-
timeout=1)
54+
num_retries=1)
5655
client.session.disconnect()
5756
del client.session
5857
del client.sock

0 commit comments

Comments
 (0)