parallel-ssh 2.11.1__py3-none-any.whl → 2.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ import gevent.pool
23
23
  from gevent import joinall, spawn, Timeout as GTimeout
24
24
  from gevent.hub import Hub
25
25
 
26
- from ..common import _validate_pkey_path
26
+ from ..common import _validate_pkey_path, _validate_pkey
27
27
  from ...config import HostConfig
28
28
  from ...constants import DEFAULT_RETRIES, RETRY_DELAY
29
29
  from ...exceptions import HostArgumentError, Timeout, ShellError, HostConfigError
@@ -39,7 +39,7 @@ class BaseParallelSSHClient(object):
39
39
  def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
40
40
  allow_agent=True,
41
41
  num_retries=DEFAULT_RETRIES,
42
- timeout=120, pool_size=10,
42
+ timeout=120, pool_size=100,
43
43
  host_config=None, retry_delay=RETRY_DELAY,
44
44
  identity_auth=True,
45
45
  ipv6_only=False,
@@ -64,7 +64,8 @@ class BaseParallelSSHClient(object):
64
64
  self.user = user
65
65
  self.password = password
66
66
  self.port = port
67
- self.pkey = pkey
67
+ self.pkey = _validate_pkey(pkey)
68
+ self.__pkey_data = self._load_pkey_data(pkey) if pkey is not None else None
68
69
  self.num_retries = num_retries
69
70
  self.timeout = timeout
70
71
  self._host_clients = {}
@@ -113,9 +114,26 @@ class BaseParallelSSHClient(object):
113
114
  self._host_clients.pop((i, host), None)
114
115
  self._hosts = _hosts
115
116
 
117
+ def __del__(self):
118
+ self.disconnect()
119
+
120
+ def disconnect(self):
121
+ """Disconnect all clients."""
122
+ if not hasattr(self, '_host_clients'):
123
+ return
124
+ for s_client in self._host_clients.values():
125
+ try:
126
+ s_client.disconnect()
127
+ except Exception as ex:
128
+ logger.debug("Client disconnect failed with %s", ex)
129
+ pass
130
+
116
131
  def _check_host_config(self):
117
132
  if self.host_config is None:
118
133
  return
134
+ if not isinstance(self.host_config, list):
135
+ raise HostConfigError("Host configuration of type %s is invalid - valid types are List[HostConfig]",
136
+ type(self.host_config))
119
137
  host_len = len(self.hosts)
120
138
  if host_len != len(self.host_config):
121
139
  raise ValueError(
@@ -231,6 +249,7 @@ class BaseParallelSSHClient(object):
231
249
 
232
250
  def _get_output_from_greenlet(self, cmd_i, cmd, raise_error=False):
233
251
  host = self.hosts[cmd_i]
252
+ alias = self._get_host_config(cmd_i).alias
234
253
  try:
235
254
  host_out = cmd.get()
236
255
  return host_out
@@ -239,8 +258,7 @@ class BaseParallelSSHClient(object):
239
258
  ex = Timeout()
240
259
  if raise_error:
241
260
  raise ex
242
- return HostOutput(host, None, None, None,
243
- exception=ex)
261
+ return HostOutput(host, None, None, None, exception=ex, alias=alias)
244
262
 
245
263
  def get_last_output(self, cmds=None):
246
264
  """Get output for last commands executed by ``run_command``.
@@ -248,7 +266,7 @@ class BaseParallelSSHClient(object):
248
266
  :param cmds: Commands to get output for. Defaults to ``client.cmds``
249
267
  :type cmds: list(:py:class:`gevent.Greenlet`)
250
268
 
251
- :rtype: dict or list
269
+ :rtype: list(:py:class:`pssh.output.HostOutput`)
252
270
  """
253
271
  cmds = self.cmds if cmds is None else cmds
254
272
  if cmds is None:
@@ -256,7 +274,7 @@ class BaseParallelSSHClient(object):
256
274
  return self._get_output_from_cmds(
257
275
  cmds, raise_error=False)
258
276
 
259
- def _get_host_config(self, host_i, host):
277
+ def _get_host_config(self, host_i):
260
278
  if self.host_config is None:
261
279
  config = HostConfig(
262
280
  user=self.user, port=self.port, password=self.password, private_key=self.pkey,
@@ -274,9 +292,6 @@ class BaseParallelSSHClient(object):
274
292
  gssapi_delegate_credentials=self.gssapi_delegate_credentials,
275
293
  )
276
294
  return config
277
- elif not isinstance(self.host_config, list):
278
- raise HostConfigError("Host configuration of type %s is invalid - valid types are list[HostConfig]",
279
- type(self.host_config))
280
295
  config = self.host_config[host_i]
281
296
  return config
282
297
 
@@ -284,7 +299,6 @@ class BaseParallelSSHClient(object):
284
299
  shell=None, use_pty=False,
285
300
  encoding='utf-8', read_timeout=None):
286
301
  """Make SSHClient if needed, run command on host"""
287
- logger.debug("_run_command with read timeout %s", read_timeout)
288
302
  try:
289
303
  _client = self._get_ssh_client(host_i, host)
290
304
  host_out = _client.run_command(
@@ -310,13 +324,13 @@ class BaseParallelSSHClient(object):
310
324
  :returns: list of greenlets to ``joinall`` with.
311
325
  :rtype: list(:py:mod:`gevent.greenlet.Greenlet`)
312
326
  """
313
- cmds = [spawn(self._get_ssh_client, i, host) for i, host in enumerate(self.hosts)]
327
+ cmds = [self.pool.spawn(self._get_ssh_client, i, host) for i, host in enumerate(self.hosts)]
314
328
  return cmds
315
329
 
316
330
  def _consume_output(self, stdout, stderr):
317
- for line in stdout:
331
+ for _ in stdout:
318
332
  pass
319
- for line in stderr:
333
+ for _ in stderr:
320
334
  pass
321
335
 
322
336
  def join(self, output=None, consume_output=False, timeout=None):
@@ -345,6 +359,9 @@ class BaseParallelSSHClient(object):
345
359
  :rtype: ``None``"""
346
360
  if output is None:
347
361
  output = self.get_last_output()
362
+ if output is None:
363
+ logger.info("No last output to join on - run_command has never been run.")
364
+ return
348
365
  elif not isinstance(output, list):
349
366
  raise ValueError("Unexpected output object type")
350
367
  cmds = [self.pool.spawn(self._join, host_out, timeout=timeout,
@@ -543,19 +560,13 @@ class BaseParallelSSHClient(object):
543
560
  return client.copy_remote_file(
544
561
  remote_file, local_file, recurse=recurse, **kwargs)
545
562
 
546
- def _handle_greenlet_exc(self, func, host, *args, **kwargs):
547
- try:
548
- return func(*args, **kwargs)
549
- except Exception as ex:
550
- raise ex
551
-
552
563
  def _get_ssh_client(self, host_i, host):
553
564
  logger.debug("Make client request for host %s, (host_i, host) in clients: %s",
554
565
  host, (host_i, host) in self._host_clients)
555
566
  _client = self._host_clients.get((host_i, host))
556
567
  if _client is not None:
557
568
  return _client
558
- cfg = self._get_host_config(host_i, host)
569
+ cfg = self._get_host_config(host_i)
559
570
  _pkey = self.pkey if cfg.private_key is None else cfg.private_key
560
571
  _pkey_data = self._load_pkey_data(_pkey)
561
572
  _client = self._make_ssh_client(host, cfg, _pkey_data)
@@ -563,12 +574,12 @@ class BaseParallelSSHClient(object):
563
574
  return _client
564
575
 
565
576
  def _load_pkey_data(self, _pkey):
566
- if isinstance(_pkey, str):
567
- _validate_pkey_path(_pkey)
568
- with open(_pkey, 'rb') as fh:
569
- _pkey_data = fh.read()
570
- return _pkey_data
571
- return _pkey
577
+ if not isinstance(_pkey, str):
578
+ return _pkey
579
+ _pkey = _validate_pkey_path(_pkey)
580
+ with open(_pkey, 'rb') as fh:
581
+ _pkey_data = fh.read()
582
+ return _pkey_data
572
583
 
573
584
  def _make_ssh_client(self, host, cfg, _pkey_data):
574
585
  raise NotImplementedError
@@ -23,19 +23,17 @@ from socket import gaierror as sock_gaierror, error as sock_error
23
23
  from gevent import sleep, socket, Timeout as GTimeout
24
24
  from gevent.hub import Hub
25
25
  from gevent.select import poll, POLLIN, POLLOUT
26
-
27
- from ssh2.utils import find_eol
28
26
  from ssh2.exceptions import AgentConnectionError, AgentListIdentitiesError, \
29
27
  AgentAuthenticationError, AgentGetIdentityError
28
+ from ssh2.utils import find_eol
30
29
 
31
30
  from ..common import _validate_pkey
32
- from ...constants import DEFAULT_RETRIES, RETRY_DELAY
33
31
  from ..reader import ConcurrentRWBuffer
32
+ from ...constants import DEFAULT_RETRIES, RETRY_DELAY
34
33
  from ...exceptions import UnknownHostError, AuthenticationError, \
35
34
  ConnectionError, Timeout, NoIPv6AddressFoundError
36
35
  from ...output import HostOutput, HostOutputBuffers, BufferData
37
36
 
38
-
39
37
  Hub.NOT_ERROR = (Exception,)
40
38
  host_logger = logging.getLogger('pssh.host_logger')
41
39
  logger = logging.getLogger(__name__)
@@ -70,6 +68,8 @@ class Stdin(object):
70
68
 
71
69
  def flush(self):
72
70
  """Flush pending data written to stdin."""
71
+ if not hasattr(self._channel, "flush"):
72
+ return
73
73
  return self._client._eagain(self._channel.flush)
74
74
 
75
75
 
@@ -159,7 +159,7 @@ class BaseSSHClient(object):
159
159
 
160
160
  def __init__(self, host,
161
161
  user=None, password=None, port=None,
162
- pkey=None,
162
+ pkey=None, alias=None,
163
163
  num_retries=DEFAULT_RETRIES,
164
164
  retry_delay=RETRY_DELAY,
165
165
  allow_agent=True, timeout=None,
@@ -171,6 +171,7 @@ class BaseSSHClient(object):
171
171
  ):
172
172
  self._auth_thread_pool = _auth_thread_pool
173
173
  self.host = host
174
+ self.alias = alias
174
175
  self.user = user if user else getuser()
175
176
  self.password = password
176
177
  self.port = port if port else 22
@@ -286,7 +287,7 @@ class BaseSSHClient(object):
286
287
  raise unknown_ex from ex
287
288
  for i, (family, _type, proto, _, sock_addr) in enumerate(addr_info):
288
289
  try:
289
- return self._connect_socket(family, _type, proto, sock_addr, host, port, retries)
290
+ return self._connect_socket(family, _type, sock_addr, host, port, retries)
290
291
  except ConnectionRefusedError as ex:
291
292
  if i+1 == len(addr_info):
292
293
  logger.error("No available addresses from %s", [addr[4] for addr in addr_info])
@@ -294,7 +295,7 @@ class BaseSSHClient(object):
294
295
  raise
295
296
  continue
296
297
 
297
- def _connect_socket(self, family, _type, proto, sock_addr, host, port, retries):
298
+ def _connect_socket(self, family, _type, sock_addr, host, port, retries):
298
299
  self.sock = socket.socket(family, _type)
299
300
  if self.timeout:
300
301
  self.sock.settimeout(self.timeout)
@@ -409,7 +410,7 @@ class BaseSSHClient(object):
409
410
  stdout=BufferData(rw_buffer=_stdout_buffer, reader=_stdout_reader),
410
411
  stderr=BufferData(rw_buffer=_stderr_buffer, reader=_stderr_reader))
411
412
  host_out = HostOutput(
412
- host=self.host, channel=channel, stdin=Stdin(channel, self),
413
+ host=self.host, alias=self.alias, channel=channel, stdin=Stdin(channel, self),
413
414
  client=self, encoding=encoding, read_timeout=read_timeout,
414
415
  buffers=_buffers,
415
416
  )
@@ -427,6 +428,8 @@ class BaseSSHClient(object):
427
428
 
428
429
  :param stderr_buffer: Buffer to read from.
429
430
  :type stderr_buffer: :py:class:`pssh.clients.reader.ConcurrentRWBuffer`
431
+ :param timeout: Timeout in seconds - defaults to no timeout.
432
+ :type timeout: int or float
430
433
  :rtype: generator
431
434
  """
432
435
  logger.debug("Reading from stderr buffer, timeout=%s", timeout)
@@ -438,6 +441,8 @@ class BaseSSHClient(object):
438
441
 
439
442
  :param stdout_buffer: Buffer to read from.
440
443
  :type stdout_buffer: :py:class:`pssh.clients.reader.ConcurrentRWBuffer`
444
+ :param timeout: Timeout in seconds - defaults to no timeout.
445
+ :type timeout: int or float
441
446
  :rtype: generator
442
447
  """
443
448
  logger.debug("Reading from stdout buffer, timeout=%s", timeout)
@@ -491,14 +496,16 @@ class BaseSSHClient(object):
491
496
  encoding='utf-8'):
492
497
  """Read from output buffers and log to ``host_logger``.
493
498
 
494
- :param output_buffer: Iterator containing buffer
499
+ :param output_buffer: Iterator containing buffer.
495
500
  :type output_buffer: iterator
496
- :param prefix: String to prefix log output to ``host_logger`` with
501
+ :param prefix: String to prefix log output to ``host_logger`` with.
497
502
  :type prefix: str
498
- :param callback: Function to call back once buffer is depleted:
503
+ :param callback: Function to call back once buffer is depleted.
499
504
  :type callback: function
500
- :param callback_args: Arguments for call back function
505
+ :param callback_args: Arguments for call back function.
501
506
  :type callback_args: tuple
507
+ :param encoding: Encoding for output.
508
+ :type encoding: str
502
509
  """
503
510
  prefix = '' if prefix is None else prefix
504
511
  for line in output_buffer:
@@ -552,7 +559,7 @@ class BaseSSHClient(object):
552
559
  host_out = self._make_host_output(channel, encoding, _timeout)
553
560
  return host_out
554
561
 
555
- def _eagain_write_errcode(self, write_func, data, eagain, timeout=None):
562
+ def _eagain_write_errcode(self, write_func, data, eagain):
556
563
  data_len = len(data)
557
564
  total_written = 0
558
565
  while total_written < data_len:
@@ -569,9 +576,10 @@ class BaseSSHClient(object):
569
576
  while ret == eagain:
570
577
  self.poll()
571
578
  ret = func(*args, **kwargs)
579
+ sleep()
572
580
  return ret
573
581
 
574
- def _eagain_write(self, write_func, data, timeout=None):
582
+ def _eagain_write(self, write_func, data):
575
583
  raise NotImplementedError
576
584
 
577
585
  def _eagain(self, func, *args, **kwargs):
@@ -127,7 +127,6 @@ class ParallelSSHClient(BaseParallelSSHClient):
127
127
  identity_auth=identity_auth,
128
128
  ipv6_only=ipv6_only,
129
129
  )
130
- self.pkey = _validate_pkey(pkey)
131
130
  self.proxy_host = proxy_host
132
131
  self.proxy_port = proxy_port
133
132
  self.proxy_pkey = _validate_pkey(proxy_pkey)
@@ -216,21 +215,11 @@ class ParallelSSHClient(BaseParallelSSHClient):
216
215
  read_timeout=read_timeout,
217
216
  )
218
217
 
219
- def __del__(self):
220
- if not hasattr(self, '_host_clients'):
221
- return
222
- for s_client in self._host_clients.values():
223
- try:
224
- s_client.disconnect()
225
- except Exception as ex:
226
- logger.debug("Client disconnect failed with %s", ex)
227
- pass
228
- del s_client
229
-
230
218
  def _make_ssh_client(self, host, cfg, _pkey_data):
231
219
  _client = SSHClient(
232
220
  host, user=cfg.user or self.user, password=cfg.password or self.password, port=cfg.port or self.port,
233
221
  pkey=_pkey_data, num_retries=cfg.num_retries or self.num_retries,
222
+ alias=cfg.alias,
234
223
  timeout=cfg.timeout or self.timeout,
235
224
  allow_agent=cfg.allow_agent or self.allow_agent, retry_delay=cfg.retry_delay or self.retry_delay,
236
225
  proxy_host=cfg.proxy_host or self.proxy_host,
@@ -370,16 +359,12 @@ class ParallelSSHClient(BaseParallelSSHClient):
370
359
  encoding=encoding)
371
360
 
372
361
  def _scp_send(self, host_i, host, local_file, remote_file, recurse=False):
373
- self._get_ssh_client(host_i, host)
374
- return self._handle_greenlet_exc(
375
- self._host_clients[(host_i, host)].scp_send, host,
376
- local_file, remote_file, recurse=recurse)
362
+ _client = self._get_ssh_client(host_i, host)
363
+ return _client.scp_send(local_file, remote_file, recurse=recurse)
377
364
 
378
365
  def _scp_recv(self, host_i, host, remote_file, local_file, recurse=False):
379
- self._get_ssh_client(host_i, host)
380
- return self._handle_greenlet_exc(
381
- self._host_clients[(host_i, host)].scp_recv, host,
382
- remote_file, local_file, recurse=recurse)
366
+ _client = self._get_ssh_client(host_i, host)
367
+ return _client.scp_recv(remote_file, local_file, recurse=recurse)
383
368
 
384
369
  def scp_send(self, local_file, remote_file, recurse=False, copy_args=None):
385
370
  """Copy local file to remote file in parallel via SCP.
@@ -404,6 +389,11 @@ class ParallelSSHClient(BaseParallelSSHClient):
404
389
  :type local_file: str
405
390
  :param remote_file: Remote filepath on remote host to copy file to
406
391
  :type remote_file: str
392
+ :param copy_args: (Optional) format local_file and remote_file strings
393
+ with per-host arguments in ``copy_args``. ``copy_args`` length must
394
+ equal length of host list -
395
+ :py:class:`pssh.exceptions.HostArgumentError` is raised otherwise
396
+ :type copy_args: tuple or list
407
397
  :param recurse: Whether or not to descend into directories recursively.
408
398
  :type recurse: bool
409
399
 
@@ -415,7 +405,7 @@ class ParallelSSHClient(BaseParallelSSHClient):
415
405
  """
416
406
  copy_args = [{'local_file': local_file,
417
407
  'remote_file': remote_file}
418
- for i, host in enumerate(self.hosts)] \
408
+ for _ in self.hosts] \
419
409
  if copy_args is None else copy_args
420
410
  local_file = "%(local_file)s"
421
411
  remote_file = "%(remote_file)s"
@@ -18,7 +18,6 @@
18
18
  import logging
19
19
  import os
20
20
  from collections import deque
21
- from warnings import warn
22
21
 
23
22
  from gevent import sleep, spawn, get_hub
24
23
  from gevent.lock import RLock
@@ -33,11 +32,10 @@ from ssh2.sftp import LIBSSH2_FXF_READ, LIBSSH2_FXF_CREAT, LIBSSH2_FXF_WRITE, \
33
32
 
34
33
  from .tunnel import FORWARDER
35
34
  from ..base.single import BaseSSHClient
36
- from ...output import HostOutput
35
+ from ...constants import DEFAULT_RETRIES, RETRY_DELAY
37
36
  from ...exceptions import SessionError, SFTPError, \
38
37
  SFTPIOError, Timeout, SCPError, ProxyError
39
- from ...constants import DEFAULT_RETRIES, RETRY_DELAY
40
-
38
+ from ...output import HostOutput
41
39
 
42
40
  logger = logging.getLogger(__name__)
43
41
  THREAD_POOL = get_hub().threadpool
@@ -50,7 +48,7 @@ class SSHClient(BaseSSHClient):
50
48
 
51
49
  def __init__(self, host,
52
50
  user=None, password=None, port=None,
53
- pkey=None,
51
+ pkey=None, alias=None,
54
52
  num_retries=DEFAULT_RETRIES,
55
53
  retry_delay=RETRY_DELAY,
56
54
  allow_agent=True, timeout=None,
@@ -64,12 +62,15 @@ class SSHClient(BaseSSHClient):
64
62
  identity_auth=True,
65
63
  ipv6_only=False,
66
64
  ):
67
- """:param host: Host name or IP to connect to.
65
+ """
66
+ :param host: Host name or IP to connect to.
68
67
  :type host: str
69
68
  :param user: User to connect as. Defaults to logged in user.
70
69
  :type user: str
71
70
  :param password: Password to use for password authentication.
72
71
  :type password: str
72
+ :param alias: Use an alias for this host.
73
+ :type alias: str
73
74
  :param port: SSH port to connect to. Defaults to SSH default (22)
74
75
  :type port: int
75
76
  :param pkey: Private key file path to use for authentication. Path must
@@ -115,6 +116,7 @@ class SSHClient(BaseSSHClient):
115
116
  self.keepalive_seconds = keepalive_seconds
116
117
  self._keepalive_greenlet = None
117
118
  self._proxy_client = None
119
+ self.alias = alias
118
120
  self.host = host
119
121
  self.port = port if port is not None else 22
120
122
  if proxy_host is not None:
@@ -131,9 +133,10 @@ class SSHClient(BaseSSHClient):
131
133
  identity_auth=identity_auth,
132
134
  )
133
135
  proxy_host = '127.0.0.1'
134
- self._chan_lock = RLock()
136
+ self._chan_stdout_lock = RLock()
137
+ self._chan_stderr_lock = RLock()
135
138
  super(SSHClient, self).__init__(
136
- host, user=user, password=password, port=port, pkey=pkey,
139
+ host, user=user, password=password, alias=alias, port=port, pkey=pkey,
137
140
  num_retries=num_retries, retry_delay=retry_delay,
138
141
  allow_agent=allow_agent, _auth_thread_pool=_auth_thread_pool,
139
142
  timeout=timeout,
@@ -146,7 +149,7 @@ class SSHClient(BaseSSHClient):
146
149
  return self._eagain(channel.shell)
147
150
 
148
151
  def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
149
- user=None, password=None,
152
+ user=None, password=None, alias=None,
150
153
  num_retries=DEFAULT_RETRIES,
151
154
  retry_delay=RETRY_DELAY,
152
155
  allow_agent=True, timeout=None,
@@ -156,7 +159,7 @@ class SSHClient(BaseSSHClient):
156
159
  assert isinstance(self.port, int)
157
160
  try:
158
161
  self._proxy_client = SSHClient(
159
- proxy_host, port=proxy_port, pkey=proxy_pkey,
162
+ proxy_host, port=proxy_port, pkey=proxy_pkey, alias=alias,
160
163
  num_retries=num_retries, user=user, password=password,
161
164
  retry_delay=retry_delay, allow_agent=allow_agent,
162
165
  timeout=timeout, forward_ssh_agent=forward_ssh_agent,
@@ -210,10 +213,12 @@ class SSHClient(BaseSSHClient):
210
213
  sleep(self._eagain(self.session.keepalive_send))
211
214
 
212
215
  def configure_keepalive(self):
216
+ """Configures keepalive on the server for `self.keepalive_seconds`."""
213
217
  self.session.keepalive_config(False, self.keepalive_seconds)
214
218
 
215
219
  def _init_session(self, retries=1):
216
220
  self.session = Session()
221
+
217
222
  if self.timeout:
218
223
  # libssh2 timeout is in ms
219
224
  self.session.set_timeout(self.timeout * 1000)
@@ -228,6 +233,8 @@ class SSHClient(BaseSSHClient):
228
233
  return self._connect_init_session_retry(retries=retries+1)
229
234
  msg = "Error connecting to host %s:%s - %s"
230
235
  logger.error(msg, self.host, self.port, ex)
236
+ if not self.sock.closed:
237
+ self.sock.close()
231
238
  if isinstance(ex, SSH2Timeout):
232
239
  raise Timeout(msg, self.host, self.port, ex)
233
240
  raise
@@ -261,16 +268,15 @@ class SSHClient(BaseSSHClient):
261
268
  return chan
262
269
 
263
270
  def open_session(self):
264
- """Open new channel from session"""
271
+ """Open new channel from session.
272
+
273
+ :rtype: :py:class:`ssh2.channel.Channel`
274
+ """
265
275
  try:
266
276
  chan = self._open_session()
267
277
  except Exception as ex:
268
278
  raise SessionError(ex)
269
- if self.forward_ssh_agent and not self._forward_requested:
270
- if not hasattr(chan, 'request_auth_agent'):
271
- warn("Requested SSH Agent forwarding but libssh2 version used "
272
- "does not support it - ignoring")
273
- return chan
279
+ # if self.forward_ssh_agent and not self._forward_requested:
274
280
  # self._eagain(chan.request_auth_agent)
275
281
  # self._forward_requested = True
276
282
  return chan
@@ -300,18 +306,19 @@ class SSHClient(BaseSSHClient):
300
306
  self._eagain(channel.execute, cmd)
301
307
  return channel
302
308
 
303
- def _read_output_to_buffer(self, read_func, _buffer):
309
+ def _read_output_to_buffer(self, read_func, _buffer, is_stderr=False):
310
+ _lock = self._chan_stderr_lock if is_stderr else self._chan_stdout_lock
304
311
  try:
305
312
  while True:
306
- with self._chan_lock:
313
+ with _lock:
307
314
  size, data = read_func()
308
- while size == LIBSSH2_ERROR_EAGAIN:
315
+ if size == LIBSSH2_ERROR_EAGAIN:
309
316
  self.poll()
310
- with self._chan_lock:
311
- size, data = read_func()
317
+ continue
312
318
  if size <= 0:
313
319
  break
314
320
  _buffer.write(data)
321
+ sleep()
315
322
  finally:
316
323
  _buffer.eof.set()
317
324
 
@@ -339,7 +346,7 @@ class SSHClient(BaseSSHClient):
339
346
  self.close_channel(channel)
340
347
 
341
348
  def close_channel(self, channel):
342
- with self._chan_lock:
349
+ with self._chan_stdout_lock, self._chan_stderr_lock:
343
350
  logger.debug("Closing channel")
344
351
  self._eagain(channel.close)
345
352
 
@@ -350,7 +357,6 @@ class SSHClient(BaseSSHClient):
350
357
  return self._eagain(self.session.sftp_init)
351
358
 
352
359
  def _make_sftp(self):
353
- """Make SFTP client from open transport"""
354
360
  try:
355
361
  sftp = self._make_sftp_eagain()
356
362
  except Exception as ex:
@@ -358,7 +364,7 @@ class SSHClient(BaseSSHClient):
358
364
  return sftp
359
365
 
360
366
  def _mkdir(self, sftp, directory):
361
- """Make directory via SFTP channel
367
+ """Make directory via SFTP channel.
362
368
 
363
369
  :param sftp: SFTP client object
364
370
  :type sftp: :py:class:`ssh2.sftp.SFTP`
@@ -428,10 +434,22 @@ class SSHClient(BaseSSHClient):
428
434
  data = local_fh.read(self._BUF_SIZE)
429
435
 
430
436
  def sftp_put(self, sftp, local_file, remote_file):
437
+ """Perform an SFTP put - copy local file path to remote via SFTP.
438
+
439
+ :param sftp: SFTP client object.
440
+ :type sftp: :py:class:`ssh2.sftp.SFTP`
441
+ :param local_file: Local filepath to copy to remote host.
442
+ :type local_file: str
443
+ :param remote_file: Remote filepath on remote host to copy file to.
444
+ :type remote_file: str
445
+
446
+ :raises: :py:class:`pssh.exceptions.SFTPIOError` on I/O errors writing
447
+ via SFTP.
448
+ """
431
449
  mode = LIBSSH2_SFTP_S_IRUSR | \
432
- LIBSSH2_SFTP_S_IWUSR | \
433
- LIBSSH2_SFTP_S_IRGRP | \
434
- LIBSSH2_SFTP_S_IROTH
450
+ LIBSSH2_SFTP_S_IWUSR | \
451
+ LIBSSH2_SFTP_S_IRGRP | \
452
+ LIBSSH2_SFTP_S_IROTH
435
453
  f_flags = LIBSSH2_FXF_CREAT | LIBSSH2_FXF_WRITE | LIBSSH2_FXF_TRUNC
436
454
  with self._sftp_openfh(
437
455
  sftp.open, remote_file, f_flags, mode) as remote_fh:
@@ -557,6 +575,9 @@ class SSHClient(BaseSSHClient):
557
575
  :type local_file: str
558
576
  :param recurse: Whether or not to recursively copy directories
559
577
  :type recurse: bool
578
+ :param sftp: The SFTP channel to use instead of creating a new one.
579
+ Only used when ``recurse`` is ``True``.
580
+ :type sftp: :py:class:`ssh2.sftp.SFTP`
560
581
  :param encoding: Encoding to use for file paths when recursion is
561
582
  enabled.
562
583
  :type encoding: str
@@ -614,6 +635,9 @@ class SSHClient(BaseSSHClient):
614
635
  :type local_file: str
615
636
  :param remote_file: Remote filepath on remote host to copy file to
616
637
  :type remote_file: str
638
+ :param sftp: The SFTP channel to use instead of creating a new one.
639
+ Only used when ``recurse`` is ``True``.
640
+ :type sftp: :py:class:`ssh2.sftp.SFTP`
617
641
  :param recurse: Whether or not to descend into directories recursively.
618
642
  :type recurse: bool
619
643
 
@@ -643,9 +667,9 @@ class SSHClient(BaseSSHClient):
643
667
  elif remote_file.endswith('/'):
644
668
  local_filename = local_file.rsplit('/')[-1]
645
669
  remote_file += local_filename
646
- self._scp_send(local_file, remote_file)
647
670
  logger.info("SCP local file %s to remote destination %s:%s",
648
671
  local_file, self.host, remote_file)
672
+ self._scp_send(local_file, remote_file)
649
673
 
650
674
  def _scp_send(self, local_file, remote_file):
651
675
  fileinfo = os.stat(local_file)
@@ -734,12 +758,12 @@ class SSHClient(BaseSSHClient):
734
758
  LIBSSH2_SESSION_BLOCK_OUTBOUND,
735
759
  )
736
760
 
737
- def _eagain_write(self, write_func, data, timeout=None):
761
+ def _eagain_write(self, write_func, data):
738
762
  """Write data with given write_func for an ssh2-python session while
739
763
  handling EAGAIN and resuming writes from last written byte on each call to
740
764
  write_func.
741
765
  """
742
- return self._eagain_write_errcode(write_func, data, LIBSSH2_ERROR_EAGAIN, timeout=timeout)
766
+ return self._eagain_write_errcode(write_func, data, LIBSSH2_ERROR_EAGAIN)
743
767
 
744
- def eagain_write(self, write_func, data, timeout=None):
745
- return self._eagain_write(write_func, data, timeout=timeout)
768
+ def eagain_write(self, write_func, data):
769
+ return self._eagain_write(write_func, data)
@@ -16,12 +16,8 @@
16
16
  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  import logging
19
-
19
+ from queue import Queue
20
20
  from threading import Thread, Event
21
- try:
22
- from queue import Queue
23
- except ImportError:
24
- from Queue import Queue
25
21
 
26
22
  from gevent import spawn, joinall, get_hub, sleep
27
23
  from gevent.server import StreamServer
@@ -29,7 +25,6 @@ from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
29
25
 
30
26
  from ...constants import DEFAULT_RETRIES
31
27
 
32
-
33
28
  logger = logging.getLogger(__name__)
34
29
 
35
30
 
@@ -193,7 +188,7 @@ class TunnelServer(StreamServer):
193
188
  sleep(.01)
194
189
  continue
195
190
  try:
196
- self._client._eagain_write(channel.write, data)
191
+ self._client.eagain_write(channel.write, data)
197
192
  except Exception as ex:
198
193
  logger.error("Error writing data to channel - %s", ex)
199
194
  raise