Skip to content

Commit 47f3caf

Browse files
authored
Read output (#227)
* Updated native single client to not use native functions. Moved polling to base client. * Updated libssh client for polling changes. * Removed native extensions * Added read timeout tests for single clients. * Updated setup.py * Simplified timeout settings. Renamed run_command timeout to read_timeout. * Updated changelog. * File copy tests cleanup their files better. * Updated dev requirements * Removed appveyor cfg.
1 parent eb0feb8 commit 47f3caf

19 files changed

+263
-8998
lines changed

.appveyor.yml

Lines changed: 0 additions & 78 deletions
This file was deleted.

Changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,19 @@ See `Upgrading to API 2.0 <upgrade-link>`_ for examples of code that will need u
2323
* Removed deprecated ``ParallelSSHClient`` ``host_config`` dictionary implementation - now list of ``HostConfig``.
2424
* Removed ``HostOutput.cmd`` attribute.
2525
* Removed ``ParallelSSHClient.host_clients`` attribute.
26+
* Made ``ParallelSSHClient(timeout=<seconds>)`` a global timeout setting for all operations.
27+
* Removed ``run_command(greenlet_timeout=<..>)`` argument - now uses global timeout setting.
28+
* Renamed ``run_command`` ``timeout`` to ``read_timeout=<seconds>)`` for setting output read timeout individually - defaults to global timeout setting.
29+
* Removed ``pssh.native`` package and native code.
30+
* No native code means package architecture has changed to ``none-any``.
2631

2732

2833
Fixes
2934
-----
3035

3136
* Removed now unnecessary locking around SSHClient initialisation so it can be parallelised - #219.
3237
* ``ParallelSSHClient.join`` with encoding would not pass on encoding when reading from output buffers - #214.
38+
* Clients could raise ``Timeout`` early when timeout settings were used with many hosts.
3339

3440

3541
1.13.0

pssh/clients/base/parallel.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import gevent.pool
2323

24-
from gevent import joinall
24+
from gevent import joinall, spawn, Timeout as GTimeout
2525
from gevent.hub import Hub
2626

2727
from ...constants import DEFAULT_RETRIES, RETRY_DELAY
@@ -82,7 +82,6 @@ def run_command(self, command, user=None, stop_on_errors=True,
8282
host_args=None, use_pty=False, shell=None,
8383
encoding='utf-8', return_list=True,
8484
*args, **kwargs):
85-
greenlet_timeout = kwargs.pop('greenlet_timeout', None)
8685
if host_args:
8786
try:
8887
cmds = [self.pool.spawn(
@@ -103,28 +102,31 @@ def run_command(self, command, user=None, stop_on_errors=True,
103102
*args, **kwargs)
104103
for host_i, host in enumerate(self.hosts)]
105104
self.cmds = cmds
106-
joinall(cmds, raise_error=False, timeout=greenlet_timeout)
107-
return self._get_output_from_cmds(cmds, stop_on_errors=stop_on_errors,
108-
timeout=greenlet_timeout,
105+
joinall(cmds, timeout=self.timeout)
106+
return self._get_output_from_cmds(cmds, raise_error=stop_on_errors,
109107
return_list=return_list)
110108

111-
def _get_output_from_cmds(self, cmds, stop_on_errors=False, timeout=None,
109+
def _get_output_from_cmds(self, cmds, raise_error=False,
112110
return_list=True):
113-
return [self._get_output_from_greenlet(cmd, timeout=timeout, raise_error=stop_on_errors)
114-
for cmd in cmds]
111+
_cmds = [spawn(self._get_output_from_greenlet, cmd, raise_error=raise_error)
112+
for cmd in cmds]
113+
finished = joinall(_cmds, raise_error=True)
114+
return [f.get() for f in finished]
115115

116-
def _get_output_from_greenlet(self, cmd, timeout=None, raise_error=False):
116+
def _get_output_from_greenlet(self, cmd, raise_error=False):
117117
try:
118-
host_out = cmd.get(timeout=timeout)
118+
host_out = cmd.get()
119119
return host_out
120-
except Exception as ex:
121-
host = ex.host
120+
except (GTimeout, Exception) as ex:
121+
host = ex.host if hasattr(ex, 'host') else None
122+
if isinstance(ex, GTimeout):
123+
ex = Timeout()
122124
if raise_error:
123125
raise ex
124126
return HostOutput(host, None, None, None, None,
125127
None, exception=ex)
126128

127-
def get_last_output(self, cmds=None, greenlet_timeout=None,
129+
def get_last_output(self, cmds=None, timeout=None,
128130
return_list=True):
129131
"""Get output for last commands executed by ``run_command``
130132
@@ -153,8 +155,8 @@ def get_last_output(self, cmds=None, greenlet_timeout=None,
153155
if cmds is None:
154156
return
155157
return self._get_output_from_cmds(
156-
cmds, timeout=greenlet_timeout, return_list=return_list,
157-
stop_on_errors=False)
158+
cmds, return_list=return_list,
159+
raise_error=False)
158160

159161
def reset_output_generators(self, host_out, timeout=None,
160162
client=None, channel=None,
@@ -205,16 +207,17 @@ def _get_host_config_values(self, host_i, host):
205207

206208
def _run_command(self, host_i, host, command, sudo=False, user=None,
207209
shell=None, use_pty=False,
208-
encoding='utf-8', timeout=None):
210+
encoding='utf-8', read_timeout=None):
209211
"""Make SSHClient if needed, run command on host"""
212+
logger.debug("_run_command with read timeout %s", read_timeout)
210213
try:
211214
_client = self._make_ssh_client(host_i, host)
212215
host_out = _client.run_command(
213216
command, sudo=sudo, user=user, shell=shell,
214-
use_pty=use_pty, encoding=encoding, timeout=timeout)
217+
use_pty=use_pty, encoding=encoding, read_timeout=read_timeout)
215218
return host_out
216-
except Exception as ex:
217-
ex.host = host
219+
except (GTimeout, Exception) as ex:
220+
host = ex.host if hasattr(ex, 'host') else None
218221
logger.error("Failed to run on host %s - %s", host, ex)
219222
raise ex
220223

pssh/clients/base/single.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from gevent import sleep, socket
2929
from gevent.hub import Hub
30+
from gevent.select import poll
3031

3132
from ..common import _validate_pkey_path
3233
from ...constants import DEFAULT_RETRIES, RETRY_DELAY
@@ -249,7 +250,7 @@ def read_output_buffer(self, output_buffer, prefix=None,
249250

250251
def run_command(self, command, sudo=False, user=None,
251252
use_pty=False, shell=None,
252-
encoding='utf-8', timeout=None):
253+
encoding='utf-8', timeout=None, read_timeout=None):
253254
"""Run remote command.
254255
255256
:param command: Command to run.
@@ -267,8 +268,10 @@ def run_command(self, command, sudo=False, user=None,
267268
:param encoding: Encoding to use for output. Must be valid
268269
`Python codec <https://docs.python.org/2.7/library/codecs.html>`_
269270
:type encoding: str
271+
:param read_timeout: (Optional) Timeout in seconds for reading output.
272+
:type read_timeout: float
270273
271-
:rtype: (channel, host, stdout, stderr, stdin) tuple.
274+
:rtype: :py:class:`pssh.output.HostOutput`
272275
"""
273276
# Fast path for no command substitution needed
274277
if not sudo and not user and not shell:
@@ -281,12 +284,13 @@ def run_command(self, command, sudo=False, user=None,
281284
_command = 'sudo -u %s -S ' % (user,)
282285
_shell = shell if shell else '$SHELL -c'
283286
_command += "%s '%s'" % (_shell, command,)
287+
_timeout = read_timeout if read_timeout else timeout
284288
channel = self.execute(_command, use_pty=use_pty)
285289
stdout = self.read_output_buffer(
286-
self.read_output(channel, timeout=timeout),
290+
self.read_output(channel, timeout=_timeout),
287291
encoding=encoding)
288292
stderr = self.read_output_buffer(
289-
self.read_stderr(channel, timeout=timeout), encoding=encoding,
293+
self.read_stderr(channel, timeout=_timeout), encoding=encoding,
290294
prefix='\t[err]')
291295
stdin = channel
292296
host_out = HostOutput(self.host, channel, stdout, stderr, stdin, self)
@@ -404,3 +408,17 @@ def _remote_paths_split(self, file_path):
404408
_sep = file_path.rfind('/')
405409
if _sep > 0:
406410
return file_path[:_sep]
411+
412+
def poll(timeout=None):
413+
raise NotImplementedError
414+
415+
def _poll_socket(self, events, timeout=None):
416+
if self.sock is None:
417+
return
418+
# gevent.select.poll converts seconds to miliseconds to match python socket
419+
# implementation
420+
timeout = timeout * 1000 if timeout is not None else 100
421+
poller = poll()
422+
poller.register(self.sock, eventmask=events)
423+
logger.debug("Polling socket with timeout %s", timeout)
424+
poller.poll(timeout=timeout)

0 commit comments

Comments
 (0)