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.
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info}/METADATA +19 -6
- parallel_ssh-2.15.0.dist-info/RECORD +27 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info}/WHEEL +1 -1
- pssh/__init__.py +13 -13
- pssh/clients/__init__.py +13 -13
- pssh/clients/base/__init__.py +16 -0
- pssh/clients/base/parallel.py +19 -28
- pssh/clients/base/single.py +105 -73
- pssh/clients/common.py +24 -14
- pssh/clients/native/__init__.py +13 -13
- pssh/clients/native/parallel.py +26 -13
- pssh/clients/native/single.py +155 -77
- pssh/clients/native/tunnel.py +77 -46
- pssh/clients/reader.py +13 -13
- pssh/clients/ssh/__init__.py +13 -13
- pssh/clients/ssh/parallel.py +18 -13
- pssh/clients/ssh/single.py +49 -32
- pssh/config.py +35 -14
- pssh/constants.py +13 -13
- pssh/exceptions.py +17 -13
- pssh/output.py +13 -13
- pssh/utils.py +13 -13
- parallel_ssh-2.13.0rc1.dist-info/RECORD +0 -27
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info/licenses}/COPYING +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info/licenses}/COPYING.LESSER +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info/licenses}/LICENSE +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.15.0.dist-info}/top_level.txt +0 -0
pssh/clients/native/parallel.py
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
#
|
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
|
-
#
|
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
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
|
pssh/clients/native/single.py
CHANGED
@@ -1,37 +1,40 @@
|
|
1
|
-
#
|
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
|
-
#
|
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
|
-
#
|
6
|
-
#
|
7
|
-
#
|
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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
|
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,
|
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.
|
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
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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
|
-
|
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
|
292
|
-
"""
|
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.
|
382
|
+
self.eagain(channel.pty)
|
305
383
|
logger.debug("Executing command '%s'", cmd)
|
306
|
-
self.
|
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
|
-
|
345
|
-
|
346
|
-
|
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.
|
431
|
+
self.eagain(channel.close)
|
352
432
|
|
353
|
-
def
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
697
|
-
self.
|
698
|
-
self.
|
699
|
-
self.
|
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.
|
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
|
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)
|