parallel-ssh 2.13.0rc1__py3-none-any.whl → 2.15.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.
@@ -1,19 +1,19 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  import logging
19
19
 
@@ -37,6 +37,8 @@ class ParallelSSHClient(BaseParallelSSHClient):
37
37
  forward_ssh_agent=False,
38
38
  keepalive_seconds=60, identity_auth=True,
39
39
  ipv6_only=False,
40
+ compress=False,
41
+ keyboard_interactive=False,
40
42
  ):
41
43
  """
42
44
  :param hosts: Hosts to connect to
@@ -115,9 +117,17 @@ class ParallelSSHClient(BaseParallelSSHClient):
115
117
  for the host(s) or raise NoIPv6AddressFoundError otherwise. Note this will
116
118
  disable connecting to an IPv4 address if an IP address is provided instead.
117
119
  :type ipv6_only: bool
120
+ :param compress: Enable/Disable compression on the client. Defaults to off.
121
+ :type compress: bool
122
+ :param keyboard_interactive: Enable/Disable keyboard interactive authentication with provided username and
123
+ password. An `InvalidAPIUse` error is raised when keyboard_interactive is enabled without a provided password.
124
+ Defaults to off.
125
+ :type keyboard_interactive: bool
118
126
 
119
127
  :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
120
128
  provided private key.
129
+ :raises: :py:class:`pssh.exceptions.InvalidAPIUseError` when `keyboard_interactive=True` with no password
130
+ provided.
121
131
  """
122
132
  BaseParallelSSHClient.__init__(
123
133
  self, hosts, user=user, password=password, port=port, pkey=pkey,
@@ -126,6 +136,8 @@ class ParallelSSHClient(BaseParallelSSHClient):
126
136
  host_config=host_config, retry_delay=retry_delay,
127
137
  identity_auth=identity_auth,
128
138
  ipv6_only=ipv6_only,
139
+ compress=compress,
140
+ keyboard_interactive=keyboard_interactive,
129
141
  )
130
142
  self.proxy_host = proxy_host
131
143
  self.proxy_port = proxy_port
@@ -232,6 +244,7 @@ class ParallelSSHClient(BaseParallelSSHClient):
232
244
  keepalive_seconds=cfg.keepalive_seconds or self.keepalive_seconds,
233
245
  identity_auth=cfg.identity_auth or self.identity_auth,
234
246
  ipv6_only=cfg.ipv6_only or self.ipv6_only,
247
+ compress=cfg.compress or self.compress,
235
248
  )
236
249
  return _client
237
250
 
@@ -1,37 +1,40 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
- import logging
19
- import os
20
18
  from collections import deque
21
19
 
22
- from gevent import sleep, spawn, get_hub
20
+ import logging
21
+ import os
22
+ from gevent import sleep, get_hub
23
23
  from gevent.lock import RLock
24
+ from gevent.pool import Pool
25
+ from gevent.socket import SHUT_RDWR
26
+ from gevent.timeout import Timeout as GTimeout
24
27
  from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
25
28
  from ssh2.exceptions import SFTPHandleError, SFTPProtocolError, \
26
29
  Timeout as SSH2Timeout
27
- from ssh2.session import Session, LIBSSH2_SESSION_BLOCK_INBOUND, LIBSSH2_SESSION_BLOCK_OUTBOUND
30
+ from ssh2.session import Session, LIBSSH2_SESSION_BLOCK_INBOUND, LIBSSH2_SESSION_BLOCK_OUTBOUND, LIBSSH2_FLAG_COMPRESS
28
31
  from ssh2.sftp import LIBSSH2_FXF_READ, LIBSSH2_FXF_CREAT, LIBSSH2_FXF_WRITE, \
29
32
  LIBSSH2_FXF_TRUNC, LIBSSH2_SFTP_S_IRUSR, LIBSSH2_SFTP_S_IRGRP, \
30
33
  LIBSSH2_SFTP_S_IWUSR, LIBSSH2_SFTP_S_IXUSR, LIBSSH2_SFTP_S_IROTH, \
31
34
  LIBSSH2_SFTP_S_IXGRP, LIBSSH2_SFTP_S_IXOTH
32
35
 
33
36
  from .tunnel import FORWARDER
34
- from ..base.single import BaseSSHClient
37
+ from ..base.single import BaseSSHClient, PollMixIn
35
38
  from ...constants import DEFAULT_RETRIES, RETRY_DELAY
36
39
  from ...exceptions import SessionError, SFTPError, \
37
40
  SFTPIOError, Timeout, SCPError, ProxyError
@@ -41,6 +44,51 @@ logger = logging.getLogger(__name__)
41
44
  THREAD_POOL = get_hub().threadpool
42
45
 
43
46
 
47
+ class KeepAlive(PollMixIn):
48
+ """Class for handling SSHClient keepalive functionality.
49
+
50
+ Spawns a greenlet in its own pool for sending keepalives to a given session.
51
+ """
52
+ __slots__ = ('sock', 'session', '_let', '_pool')
53
+
54
+ def __init__(self, sock, session):
55
+ """
56
+ :param sock: The socket session is using to communicate.
57
+ :type sock: :py:class:`gevent.socket.socket`
58
+ :param session: The session keepalive is configured on.
59
+ :type session: :py:class:`ssh2.session.Session`
60
+ """
61
+ super(PollMixIn, self).__init__()
62
+ self._pool = Pool(1)
63
+ self.sock = sock
64
+ self.session = session
65
+ self._let = self._pool.spawn(self._send_keepalive)
66
+ self._let.start()
67
+
68
+ def _send_keepalive(self):
69
+ while True:
70
+ if self.session is None or self.sock is None or self.sock.closed:
71
+ return
72
+ sleep_for = self.eagain(self.session.keepalive_send)
73
+ sleep(sleep_for)
74
+
75
+ def poll(self, timeout=None):
76
+ """Perform co-operative gevent poll on ssh2 session socket.
77
+
78
+ Blocks current greenlet only if socket has pending read or write operations
79
+ in the appropriate direction.
80
+ :param timeout: Deprecated and unused - to be removed.
81
+ """
82
+ self._poll_errcodes(
83
+ self.session.block_directions,
84
+ LIBSSH2_SESSION_BLOCK_INBOUND,
85
+ LIBSSH2_SESSION_BLOCK_OUTBOUND,
86
+ )
87
+
88
+ def eagain(self, func, *args, **kwargs):
89
+ return self._eagain_errcode(func, LIBSSH2_ERROR_EAGAIN, *args, **kwargs)
90
+
91
+
44
92
  class SSHClient(BaseSSHClient):
45
93
  """ssh2-python (libssh2) based non-blocking SSH client."""
46
94
  # 2MB buffer
@@ -58,9 +106,12 @@ class SSHClient(BaseSSHClient):
58
106
  proxy_pkey=None,
59
107
  proxy_user=None,
60
108
  proxy_password=None,
61
- _auth_thread_pool=True, keepalive_seconds=60,
109
+ _auth_thread_pool=True,
110
+ keepalive_seconds=60,
62
111
  identity_auth=True,
63
112
  ipv6_only=False,
113
+ compress=False,
114
+ keyboard_interactive=False,
64
115
  ):
65
116
  """
66
117
  :param host: Host name or IP to connect to.
@@ -85,7 +136,9 @@ class SSHClient(BaseSSHClient):
85
136
  to :py:class:`pssh.constants.RETRY_DELAY`
86
137
  :type retry_delay: int or float
87
138
  :param timeout: SSH session timeout setting in seconds. This controls
88
- timeout setting of authenticated SSH sessions.
139
+ timeout setting of authenticated SSH sessions for each individual SSH operation.
140
+ Also currently sets socket as well as per function timeout in some cases, see
141
+ function descriptions.
89
142
  :type timeout: int or float
90
143
  :param allow_agent: (Optional) set to False to disable connecting to
91
144
  the system's SSH agent
@@ -107,9 +160,17 @@ class SSHClient(BaseSSHClient):
107
160
  for the host or raise NoIPv6AddressFoundError otherwise. Note this will
108
161
  disable connecting to an IPv4 address if an IP address is provided instead.
109
162
  :type ipv6_only: bool
163
+ :param compress: Enable/Disable compression on the client. Defaults to off.
164
+ :type compress: bool
165
+ :param keyboard_interactive: Enable/Disable keyboard interactive authentication with provided username and
166
+ password. An `InvalidAPIUse` error is raised when keyboard_interactive is enabled without a provided password.
167
+ Defaults to off.
168
+ :type keyboard_interactive: bool
110
169
 
111
170
  :raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
112
171
  provided private key.
172
+ :raises: :py:class:`pssh.exceptions.InvalidAPIUseError` when `keyboard_interactive=True` with no password
173
+ provided.
113
174
  """
114
175
  self.forward_ssh_agent = forward_ssh_agent
115
176
  self._forward_requested = False
@@ -131,6 +192,8 @@ class SSHClient(BaseSSHClient):
131
192
  timeout=timeout,
132
193
  keepalive_seconds=keepalive_seconds,
133
194
  identity_auth=identity_auth,
195
+ compress=compress,
196
+ keyboard_interactive=keyboard_interactive,
134
197
  )
135
198
  proxy_host = '127.0.0.1'
136
199
  self._chan_stdout_lock = RLock()
@@ -143,10 +206,12 @@ class SSHClient(BaseSSHClient):
143
206
  proxy_host=proxy_host, proxy_port=proxy_port,
144
207
  identity_auth=identity_auth,
145
208
  ipv6_only=ipv6_only,
209
+ compress=compress,
210
+ keyboard_interactive=keyboard_interactive,
146
211
  )
147
212
 
148
213
  def _shell(self, channel):
149
- return self._eagain(channel.shell)
214
+ return self.eagain(channel.shell)
150
215
 
151
216
  def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
152
217
  user=None, password=None, alias=None,
@@ -155,7 +220,10 @@ class SSHClient(BaseSSHClient):
155
220
  allow_agent=True, timeout=None,
156
221
  forward_ssh_agent=False,
157
222
  keepalive_seconds=60,
158
- identity_auth=True):
223
+ identity_auth=True,
224
+ compress=False,
225
+ keyboard_interactive=False,
226
+ ):
159
227
  assert isinstance(self.port, int)
160
228
  try:
161
229
  self._proxy_client = SSHClient(
@@ -165,6 +233,8 @@ class SSHClient(BaseSSHClient):
165
233
  timeout=timeout, forward_ssh_agent=forward_ssh_agent,
166
234
  identity_auth=identity_auth,
167
235
  keepalive_seconds=keepalive_seconds,
236
+ compress=compress,
237
+ keyboard_interactive=keyboard_interactive,
168
238
  _auth_thread_pool=False)
169
239
  except Exception as ex:
170
240
  msg = "Proxy authentication failed. " \
@@ -178,46 +248,42 @@ class SSHClient(BaseSSHClient):
178
248
  proxy_local_port = FORWARDER.out_q.get()
179
249
  return proxy_local_port
180
250
 
181
- def disconnect(self):
251
+ def _disconnect(self):
182
252
  """Attempt to disconnect session.
183
253
 
184
254
  Any errors on calling disconnect are suppressed by this function.
255
+
256
+ Does not need to be called directly - called when client object is de-allocated.
185
257
  """
186
258
  self._keepalive_greenlet = None
187
- if self.session is not None:
259
+ if self.session is not None and self.sock is not None and not self.sock.closed:
188
260
  try:
189
261
  self._disconnect_eagain()
190
262
  except Exception:
191
263
  pass
192
- self.session = None
264
+ self.session = None
265
+ # To allow for file descriptor reuse, which is part of gevent, shutdown but do not close socket here.
266
+ # Done by gevent when file descriptor is closed.
267
+ if self.sock is not None and not self.sock.closed:
268
+ try:
269
+ self.sock.shutdown(SHUT_RDWR)
270
+ self.sock.detach()
271
+ except Exception:
272
+ pass
273
+ self.sock = None
274
+ # Notify forwarder that proxy tunnel server can be shutdown
193
275
  if isinstance(self._proxy_client, SSHClient):
194
- # Don't disconnect proxy client here - let the TunnelServer do it at the time that
195
- # _wait_send_receive_lets ends. The cleanup_server call here triggers the TunnelServer
196
- # to stop.
197
276
  FORWARDER.cleanup_server(self._proxy_client)
198
277
 
199
- # I wanted to clean up all the sockets here to avoid a ResourceWarning from unittest,
200
- # but unfortunately closing this socket here causes a segfault, not sure why yet.
201
- # self.sock.close()
202
- else:
203
- self.sock.close()
204
- self.sock = None
205
-
206
- def spawn_send_keepalive(self):
207
- """Spawns a new greenlet that sends keep alive messages every
208
- self.keepalive_seconds"""
209
- return spawn(self._send_keepalive)
210
-
211
- def _send_keepalive(self):
212
- while True:
213
- sleep(self._eagain(self.session.keepalive_send))
214
-
215
278
  def configure_keepalive(self):
216
279
  """Configures keepalive on the server for `self.keepalive_seconds`."""
280
+ # Configure keepalives without a reply.
217
281
  self.session.keepalive_config(False, self.keepalive_seconds)
218
282
 
219
283
  def _init_session(self, retries=1):
220
284
  self.session = Session()
285
+ if self.compress:
286
+ self.session.flag(LIBSSH2_FLAG_COMPRESS)
221
287
 
222
288
  if self.timeout:
223
289
  # libssh2 timeout is in ms
@@ -234,7 +300,11 @@ class SSHClient(BaseSSHClient):
234
300
  msg = "Error connecting to host %s:%s - %s"
235
301
  logger.error(msg, self.host, self.port, ex)
236
302
  if not self.sock.closed:
237
- self.sock.close()
303
+ try:
304
+ self.sock.shutdown(SHUT_RDWR)
305
+ self.sock.detach()
306
+ except Exception:
307
+ pass
238
308
  if isinstance(ex, SSH2Timeout):
239
309
  raise Timeout(msg, self.host, self.port, ex)
240
310
  raise
@@ -242,7 +312,7 @@ class SSHClient(BaseSSHClient):
242
312
  def _keepalive(self):
243
313
  if self.keepalive_seconds:
244
314
  self.configure_keepalive()
245
- self._keepalive_greenlet = self.spawn_send_keepalive()
315
+ self._keepalive_greenlet = KeepAlive(self.sock, self.session)
246
316
 
247
317
  def _agent_auth(self):
248
318
  self.session.agent_auth(self.user)
@@ -261,10 +331,12 @@ class SSHClient(BaseSSHClient):
261
331
  )
262
332
 
263
333
  def _password_auth(self):
264
- self.session.userauth_password(self.user, self.password)
334
+ if self.keyboard_interactive:
335
+ return self.session.userauth_keyboardinteractive(self.user, self.password)
336
+ return self.session.userauth_password(self.user, self.password)
265
337
 
266
338
  def _open_session(self):
267
- chan = self._eagain(self.session.open_session)
339
+ chan = self.eagain(self.session.open_session)
268
340
  return chan
269
341
 
270
342
  def open_session(self):
@@ -282,14 +354,18 @@ class SSHClient(BaseSSHClient):
282
354
  return chan
283
355
 
284
356
  def _make_output_readers(self, channel, stdout_buffer, stderr_buffer):
285
- _stdout_reader = spawn(
357
+ # TODO: These greenlets need to be outside client scope or we create a reader <-> client cyclical reference
358
+ _stdout_reader = self._pool.spawn(
286
359
  self._read_output_to_buffer, channel.read, stdout_buffer)
287
- _stderr_reader = spawn(
360
+ _stderr_reader = self._pool.spawn(
288
361
  self._read_output_to_buffer, channel.read_stderr, stderr_buffer)
289
362
  return _stdout_reader, _stderr_reader
290
363
 
291
- def execute(self, cmd, use_pty=False, channel=None):
292
- """Execute command on remote server.
364
+ def _execute(self, cmd, use_pty=False, channel=None):
365
+ """
366
+ Use ``run_command`` which returns a ``HostOutput`` object rather than this function directly.
367
+
368
+ Execute command on remote server.
293
369
 
294
370
  :param cmd: Command to execute.
295
371
  :type cmd: str
@@ -298,12 +374,14 @@ class SSHClient(BaseSSHClient):
298
374
  :param channel: Use provided channel for execute rather than creating
299
375
  a new one.
300
376
  :type channel: :py:class:`ssh2.channel.Channel`
377
+
378
+ :rtype: :py:class:`ssh2.channel.Channel`
301
379
  """
302
380
  channel = self.open_session() if channel is None else channel
303
381
  if use_pty:
304
- self._eagain(channel.pty)
382
+ self.eagain(channel.pty)
305
383
  logger.debug("Executing command '%s'", cmd)
306
- self._eagain(channel.execute, cmd)
384
+ self.eagain(channel.execute, cmd)
307
385
  return channel
308
386
 
309
387
  def _read_output_to_buffer(self, read_func, _buffer, is_stderr=False):
@@ -341,20 +419,23 @@ class SSHClient(BaseSSHClient):
341
419
  channel = host_output.channel
342
420
  if channel is None:
343
421
  return
344
- self._eagain(channel.wait_eof, timeout=timeout)
345
- # Close channel to indicate no more commands will be sent over it
346
- self.close_channel(channel)
422
+ with GTimeout(seconds=timeout, exception=Timeout):
423
+ self.eagain(channel.wait_eof)
424
+ # Close channel to indicate no more commands will be sent over it
425
+ self.close_channel(channel)
347
426
 
348
427
  def close_channel(self, channel):
428
+ """Close given channel, handling EAGAIN."""
349
429
  with self._chan_stdout_lock, self._chan_stderr_lock:
350
430
  logger.debug("Closing channel")
351
- self._eagain(channel.close)
431
+ self.eagain(channel.close)
352
432
 
353
- def _eagain(self, func, *args, **kwargs):
433
+ def eagain(self, func, *args, **kwargs):
434
+ """Handle EAGAIN and call given function with any args, polling for as long as there is data to receive."""
354
435
  return self._eagain_errcode(func, LIBSSH2_ERROR_EAGAIN, *args, **kwargs)
355
436
 
356
437
  def _make_sftp_eagain(self):
357
- return self._eagain(self.session.sftp_init)
438
+ return self.eagain(self.session.sftp_init)
358
439
 
359
440
  def _make_sftp(self):
360
441
  try:
@@ -381,7 +462,7 @@ class SSHClient(BaseSSHClient):
381
462
  LIBSSH2_SFTP_S_IXGRP | \
382
463
  LIBSSH2_SFTP_S_IXOTH
383
464
  try:
384
- self._eagain(sftp.mkdir, directory, mode)
465
+ self.eagain(sftp.mkdir, directory, mode)
385
466
  except SFTPProtocolError as error:
386
467
  msg = "Error occured creating directory %s on host %s - %s"
387
468
  logger.error(msg, directory, self.host, error)
@@ -419,7 +500,7 @@ class SSHClient(BaseSSHClient):
419
500
  destination = self._remote_paths_split(remote_file)
420
501
  if destination is not None:
421
502
  try:
422
- self._eagain(sftp.stat, destination)
503
+ self.eagain(sftp.stat, destination)
423
504
  except (SFTPHandleError, SFTPProtocolError):
424
505
  self.mkdir(sftp, destination)
425
506
  self.sftp_put(sftp, local_file, remote_file)
@@ -482,7 +563,7 @@ class SSHClient(BaseSSHClient):
482
563
  cur_dir = _paths_to_create.popleft()
483
564
  cwd = '/'.join([cwd, cur_dir])
484
565
  try:
485
- self._eagain(sftp.stat, cwd)
566
+ self.eagain(sftp.stat, cwd)
486
567
  except (SFTPHandleError, SFTPProtocolError) as ex:
487
568
  logger.debug("Stat for %s failed with %s", cwd, ex)
488
569
  self._mkdir(sftp, cwd)
@@ -514,7 +595,7 @@ class SSHClient(BaseSSHClient):
514
595
  """
515
596
  sftp = self._make_sftp() if sftp is None else sftp
516
597
  try:
517
- self._eagain(sftp.stat, remote_file)
598
+ self.eagain(sftp.stat, remote_file)
518
599
  except (SFTPHandleError, SFTPProtocolError):
519
600
  msg = "Remote file or directory %s on host %s does not exist"
520
601
  logger.error(msg, remote_file, self.host)
@@ -541,7 +622,7 @@ class SSHClient(BaseSSHClient):
541
622
 
542
623
  def _scp_recv_recursive(self, remote_file, local_file, sftp, encoding='utf-8'):
543
624
  try:
544
- self._eagain(sftp.stat, remote_file)
625
+ self.eagain(sftp.stat, remote_file)
545
626
  except (SFTPHandleError, SFTPProtocolError):
546
627
  msg = "Remote file or directory %s does not exist"
547
628
  logger.error(msg, remote_file)
@@ -602,7 +683,7 @@ class SSHClient(BaseSSHClient):
602
683
 
603
684
  def _scp_recv(self, remote_file, local_file):
604
685
  try:
605
- (file_chan, fileinfo) = self._eagain(
686
+ (file_chan, fileinfo) = self.eagain(
606
687
  self.session.scp_recv2, remote_file)
607
688
  except Exception as ex:
608
689
  msg = "Error copying file %s from host %s - %s"
@@ -661,7 +742,7 @@ class SSHClient(BaseSSHClient):
661
742
  if destination is not None:
662
743
  sftp = self._make_sftp() if sftp is None else sftp
663
744
  try:
664
- self._eagain(sftp.stat, destination)
745
+ self.eagain(sftp.stat, destination)
665
746
  except (SFTPHandleError, SFTPProtocolError):
666
747
  self.mkdir(sftp, destination)
667
748
  elif remote_file.endswith('/'):
@@ -674,7 +755,7 @@ class SSHClient(BaseSSHClient):
674
755
  def _scp_send(self, local_file, remote_file):
675
756
  fileinfo = os.stat(local_file)
676
757
  try:
677
- chan = self._eagain(
758
+ chan = self.eagain(
678
759
  self.session.scp_send64,
679
760
  remote_file, fileinfo.st_mode & 0o777, fileinfo.st_size,
680
761
  fileinfo.st_mtime, fileinfo.st_atime)
@@ -693,14 +774,14 @@ class SSHClient(BaseSSHClient):
693
774
  logger.error(msg, remote_file, self.host, ex)
694
775
  raise SCPError(msg, remote_file, self.host, ex)
695
776
  finally:
696
- self._eagain(chan.flush)
697
- self._eagain(chan.send_eof)
698
- self._eagain(chan.wait_eof)
699
- self._eagain(chan.wait_closed)
777
+ self.eagain(chan.flush)
778
+ self.eagain(chan.send_eof)
779
+ self.eagain(chan.wait_eof)
780
+ self.eagain(chan.wait_closed)
700
781
 
701
782
  def _sftp_openfh(self, open_func, remote_file, *args):
702
783
  try:
703
- fh = self._eagain(open_func, remote_file, *args)
784
+ fh = self.eagain(open_func, remote_file, *args)
704
785
  except Exception as ex:
705
786
  raise SFTPError(ex)
706
787
  return fh
@@ -758,12 +839,9 @@ class SSHClient(BaseSSHClient):
758
839
  LIBSSH2_SESSION_BLOCK_OUTBOUND,
759
840
  )
760
841
 
761
- def _eagain_write(self, write_func, data):
842
+ def eagain_write(self, write_func, data):
762
843
  """Write data with given write_func for an ssh2-python session while
763
844
  handling EAGAIN and resuming writes from last written byte on each call to
764
845
  write_func.
765
846
  """
766
847
  return self._eagain_write_errcode(write_func, data, LIBSSH2_ERROR_EAGAIN)
767
-
768
- def eagain_write(self, write_func, data):
769
- return self._eagain_write(write_func, data)