parallel-ssh 2.13.0rc1__py3-none-any.whl → 2.14.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.14.0.dist-info}/METADATA +1 -1
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/RECORD +12 -12
- pssh/clients/base/parallel.py +0 -14
- pssh/clients/base/single.py +84 -58
- pssh/clients/native/single.py +116 -61
- pssh/clients/native/tunnel.py +64 -33
- pssh/clients/ssh/single.py +30 -19
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/COPYING +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/COPYING.LESSER +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/LICENSE +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/WHEEL +0 -0
- {parallel_ssh-2.13.0rc1.dist-info → parallel_ssh-2.14.0.dist-info}/top_level.txt +0 -0
@@ -9,19 +9,19 @@ pssh/clients/__init__.py,sha256=QzNb1FcbjQyxEPtRbog-ZOidMRu0zA718PkRz21SMmQ,836
|
|
9
9
|
pssh/clients/common.py,sha256=fd_jJj8ezDjwr3peN3LonLff5EYSNkMEre0yjlnRjBU,1375
|
10
10
|
pssh/clients/reader.py,sha256=9boQMv9xUBGLY53OybJmWvfa6qsA6mudfdfXk4sF2Oc,3092
|
11
11
|
pssh/clients/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
-
pssh/clients/base/parallel.py,sha256=
|
13
|
-
pssh/clients/base/single.py,sha256=
|
12
|
+
pssh/clients/base/parallel.py,sha256=GZEYNpg-sOCfKcM-jaAaxdW2Ln4_A_f77qhmCbsdll8,24595
|
13
|
+
pssh/clients/base/single.py,sha256=_Txr04S11LADbRekTvpS2xBf4C9UV3rTEZ22tPOTdfo,27013
|
14
14
|
pssh/clients/native/__init__.py,sha256=dydX5Fae9U9gRBT8koPTM8H9u4t0rIgD1q9XammFYSs,865
|
15
15
|
pssh/clients/native/parallel.py,sha256=AjOJoLdhyyUKWp4Leda08cXC-SsyT16XK4PExiOl3-c,24119
|
16
|
-
pssh/clients/native/single.py,sha256=
|
17
|
-
pssh/clients/native/tunnel.py,sha256=
|
16
|
+
pssh/clients/native/single.py,sha256=AQWmq_uBUFhZDkX6Cn0jR21BVd8twcYu_UhwMJYex9U,34388
|
17
|
+
pssh/clients/native/tunnel.py,sha256=xMfn75O32E-ZzVTFkA7DeHZOO0MXFSGn4aqGHNIb2lc,10650
|
18
18
|
pssh/clients/ssh/__init__.py,sha256=HgYZkVJ5QOgaVFp5snwyLZvQbp0Tbdxj8Gbyic2_Ibo,857
|
19
19
|
pssh/clients/ssh/parallel.py,sha256=2X47tgPklubHbb7RjUk75IZjidbbOdJq3VstH3KU6KM,11397
|
20
|
-
pssh/clients/ssh/single.py,sha256=
|
21
|
-
parallel_ssh-2.
|
22
|
-
parallel_ssh-2.
|
23
|
-
parallel_ssh-2.
|
24
|
-
parallel_ssh-2.
|
25
|
-
parallel_ssh-2.
|
26
|
-
parallel_ssh-2.
|
27
|
-
parallel_ssh-2.
|
20
|
+
pssh/clients/ssh/single.py,sha256=z9_OQKYyxOMnTixycZLuBoU2GjQYO_I-VL_L3GddwKo,14068
|
21
|
+
parallel_ssh-2.14.0.dist-info/COPYING,sha256=ZA2Q9u5AEkH_YoNNDRsz-DBJ6ZuL_foE7RsKFjXd4-c,18093
|
22
|
+
parallel_ssh-2.14.0.dist-info/COPYING.LESSER,sha256=AKibDRiqzUEU3s95Ei24e_Nb3a8rxQ44PJyfTCYzkLI,24486
|
23
|
+
parallel_ssh-2.14.0.dist-info/LICENSE,sha256=m4cqigcLitMpxL04D7G_AAD1ZMdQI-yOHmgD8VNkuek,26461
|
24
|
+
parallel_ssh-2.14.0.dist-info/METADATA,sha256=Y2Vjl44EdOIuyRpZx2Feav-OLD_6yiiB3u6ly5LgVGc,10730
|
25
|
+
parallel_ssh-2.14.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
26
|
+
parallel_ssh-2.14.0.dist-info/top_level.txt,sha256=s8P6ZHOwt2BYgDc62Cpd2z7i-rebGzIhhnO09pger0U,5
|
27
|
+
parallel_ssh-2.14.0.dist-info/RECORD,,
|
pssh/clients/base/parallel.py
CHANGED
@@ -114,20 +114,6 @@ class BaseParallelSSHClient(object):
|
|
114
114
|
self._host_clients.pop((i, host), None)
|
115
115
|
self._hosts = _hosts
|
116
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
|
-
|
131
117
|
def _check_host_config(self):
|
132
118
|
if self.host_config is None:
|
133
119
|
return
|
pssh/clients/base/single.py
CHANGED
@@ -19,10 +19,13 @@ import logging
|
|
19
19
|
import os
|
20
20
|
from getpass import getuser
|
21
21
|
from socket import gaierror as sock_gaierror, error as sock_error
|
22
|
+
from warnings import warn
|
22
23
|
|
23
24
|
from gevent import sleep, socket, Timeout as GTimeout
|
24
25
|
from gevent.hub import Hub
|
25
26
|
from gevent.select import poll, POLLIN, POLLOUT
|
27
|
+
from gevent.socket import SHUT_RDWR
|
28
|
+
from gevent.pool import Pool
|
26
29
|
from ssh2.exceptions import AgentConnectionError, AgentListIdentitiesError, \
|
27
30
|
AgentAuthenticationError, AgentGetIdentityError
|
28
31
|
from ssh2.utils import find_eol
|
@@ -39,6 +42,61 @@ host_logger = logging.getLogger('pssh.host_logger')
|
|
39
42
|
logger = logging.getLogger(__name__)
|
40
43
|
|
41
44
|
|
45
|
+
class PollMixIn(object):
|
46
|
+
"""MixIn for co-operative socket polling functionality.
|
47
|
+
|
48
|
+
"""
|
49
|
+
__slots__ = ('sock',)
|
50
|
+
|
51
|
+
def __init__(self, sock=None):
|
52
|
+
self.sock = sock
|
53
|
+
|
54
|
+
def poll(self, timeout=None):
|
55
|
+
raise NotImplementedError
|
56
|
+
|
57
|
+
def eagain(self, func, *args, **kwargs):
|
58
|
+
raise NotImplementedError
|
59
|
+
|
60
|
+
def eagain_write(self, write_func, data):
|
61
|
+
raise NotImplementedError
|
62
|
+
|
63
|
+
def _poll_errcodes(self, directions_func, inbound, outbound):
|
64
|
+
directions = directions_func()
|
65
|
+
if directions == 0:
|
66
|
+
return
|
67
|
+
events = 0
|
68
|
+
if directions & inbound:
|
69
|
+
events = POLLIN
|
70
|
+
if directions & outbound:
|
71
|
+
events |= POLLOUT
|
72
|
+
self._poll_socket(events)
|
73
|
+
|
74
|
+
def _poll_socket(self, events):
|
75
|
+
if self.sock is None:
|
76
|
+
return
|
77
|
+
poller = poll()
|
78
|
+
poller.register(self.sock, eventmask=events)
|
79
|
+
poller.poll(timeout=1)
|
80
|
+
|
81
|
+
def _eagain_errcode(self, func, eagain, *args, **kwargs):
|
82
|
+
ret = func(*args, **kwargs)
|
83
|
+
while ret == eagain:
|
84
|
+
self.poll()
|
85
|
+
ret = func(*args, **kwargs)
|
86
|
+
sleep()
|
87
|
+
return ret
|
88
|
+
|
89
|
+
def _eagain_write_errcode(self, write_func, data, eagain):
|
90
|
+
data_len = len(data)
|
91
|
+
total_written = 0
|
92
|
+
while total_written < data_len:
|
93
|
+
rc, bytes_written = write_func(data[total_written:])
|
94
|
+
total_written += bytes_written
|
95
|
+
if rc == eagain:
|
96
|
+
self.poll()
|
97
|
+
sleep()
|
98
|
+
|
99
|
+
|
42
100
|
class Stdin(object):
|
43
101
|
"""Stdin stream for a channel.
|
44
102
|
|
@@ -64,13 +122,13 @@ class Stdin(object):
|
|
64
122
|
:param data: Data to write.
|
65
123
|
:type data: str
|
66
124
|
"""
|
67
|
-
return self._client.
|
125
|
+
return self._client.eagain(self._channel.write, data)
|
68
126
|
|
69
127
|
def flush(self):
|
70
128
|
"""Flush pending data written to stdin."""
|
71
129
|
if not hasattr(self._channel, "flush"):
|
72
130
|
return
|
73
|
-
return self._client.
|
131
|
+
return self._client.eagain(self._channel.flush)
|
74
132
|
|
75
133
|
|
76
134
|
class InteractiveShell(object):
|
@@ -132,7 +190,7 @@ class InteractiveShell(object):
|
|
132
190
|
"""Wait for shell to finish executing and close channel."""
|
133
191
|
if self._chan is None:
|
134
192
|
return
|
135
|
-
self._client.
|
193
|
+
self._client.eagain(self._chan.send_eof)
|
136
194
|
self._client.wait_finished(self.output)
|
137
195
|
return self
|
138
196
|
|
@@ -144,10 +202,10 @@ class InteractiveShell(object):
|
|
144
202
|
:type cmd: str
|
145
203
|
"""
|
146
204
|
cmd = cmd.encode(self._encoding) + self._EOL
|
147
|
-
self._client.
|
205
|
+
self._client.eagain_write(self._chan.write, cmd)
|
148
206
|
|
149
207
|
|
150
|
-
class BaseSSHClient(
|
208
|
+
class BaseSSHClient(PollMixIn):
|
151
209
|
|
152
210
|
IDENTITIES = (
|
153
211
|
os.path.expanduser('~/.ssh/id_rsa'),
|
@@ -169,6 +227,7 @@ class BaseSSHClient(object):
|
|
169
227
|
identity_auth=True,
|
170
228
|
ipv6_only=False,
|
171
229
|
):
|
230
|
+
super(PollMixIn, self).__init__()
|
172
231
|
self._auth_thread_pool = _auth_thread_pool
|
173
232
|
self.host = host
|
174
233
|
self.alias = alias
|
@@ -176,7 +235,6 @@ class BaseSSHClient(object):
|
|
176
235
|
self.password = password
|
177
236
|
self.port = port if port else 22
|
178
237
|
self.num_retries = num_retries
|
179
|
-
self.sock = None
|
180
238
|
self.timeout = timeout if timeout else None
|
181
239
|
self.retry_delay = retry_delay
|
182
240
|
self.allow_agent = allow_agent
|
@@ -187,6 +245,7 @@ class BaseSSHClient(object):
|
|
187
245
|
self.identity_auth = identity_auth
|
188
246
|
self._keepalive_greenlet = None
|
189
247
|
self.ipv6_only = ipv6_only
|
248
|
+
self._pool = Pool()
|
190
249
|
self._init()
|
191
250
|
|
192
251
|
def _pkey_from_memory(self, pkey_data):
|
@@ -212,11 +271,15 @@ class BaseSSHClient(object):
|
|
212
271
|
raise AuthenticationError(msg, self.host, self.port, ex, retries, self.num_retries)
|
213
272
|
|
214
273
|
def disconnect(self):
|
274
|
+
"""Deprecated and a no-op. Disconnections handled by client de-allocation."""
|
275
|
+
warn("Deprecated and a no-op - to be removed in future releases.", DeprecationWarning)
|
276
|
+
|
277
|
+
def _disconnect(self):
|
215
278
|
raise NotImplementedError
|
216
279
|
|
217
280
|
def __del__(self):
|
218
281
|
try:
|
219
|
-
self.
|
282
|
+
self._disconnect()
|
220
283
|
except Exception:
|
221
284
|
pass
|
222
285
|
|
@@ -224,7 +287,7 @@ class BaseSSHClient(object):
|
|
224
287
|
return self
|
225
288
|
|
226
289
|
def __exit__(self, *args):
|
227
|
-
self.
|
290
|
+
self._disconnect()
|
228
291
|
|
229
292
|
def open_shell(self, encoding='utf-8', read_timeout=None):
|
230
293
|
"""Open interactive shell on new channel.
|
@@ -244,7 +307,8 @@ class BaseSSHClient(object):
|
|
244
307
|
raise NotImplementedError
|
245
308
|
|
246
309
|
def _disconnect_eagain(self):
|
247
|
-
self.
|
310
|
+
if self.session is not None and self.sock is not None and not self.sock.closed:
|
311
|
+
self.eagain(self.session.disconnect)
|
248
312
|
|
249
313
|
def _connect_init_session_retry(self, retries):
|
250
314
|
try:
|
@@ -254,9 +318,11 @@ class BaseSSHClient(object):
|
|
254
318
|
self.session = None
|
255
319
|
if not self.sock.closed:
|
256
320
|
try:
|
257
|
-
self.sock.
|
321
|
+
self.sock.shutdown(SHUT_RDWR)
|
322
|
+
self.sock.detach()
|
258
323
|
except Exception:
|
259
324
|
pass
|
325
|
+
self.sock = None
|
260
326
|
sleep(self.retry_delay)
|
261
327
|
self._connect(self._host, self._port, retries=retries)
|
262
328
|
return self._init_session(retries=retries)
|
@@ -420,6 +486,13 @@ class BaseSSHClient(object):
|
|
420
486
|
raise NotImplementedError
|
421
487
|
|
422
488
|
def execute(self, cmd, use_pty=False, channel=None):
|
489
|
+
"""
|
490
|
+
Deprecated - use ``run_command`` instead which returns a ``HostOutput`` object.
|
491
|
+
"""
|
492
|
+
warn("Deprecated - use run_command instead.", DeprecationWarning)
|
493
|
+
return self._execute(cmd, use_pty=use_pty, channel=channel)
|
494
|
+
|
495
|
+
def _execute(self, cmd, use_pty=False, channel=None):
|
423
496
|
raise NotImplementedError
|
424
497
|
|
425
498
|
def read_stderr(self, stderr_buffer, timeout=None):
|
@@ -554,37 +627,11 @@ class BaseSSHClient(object):
|
|
554
627
|
_command += "%s '%s'" % (_shell, command,)
|
555
628
|
_command = _command.encode(encoding)
|
556
629
|
with GTimeout(seconds=self.timeout):
|
557
|
-
channel = self.
|
630
|
+
channel = self._execute(_command, use_pty=use_pty)
|
558
631
|
_timeout = read_timeout if read_timeout else timeout
|
559
632
|
host_out = self._make_host_output(channel, encoding, _timeout)
|
560
633
|
return host_out
|
561
634
|
|
562
|
-
def _eagain_write_errcode(self, write_func, data, eagain):
|
563
|
-
data_len = len(data)
|
564
|
-
total_written = 0
|
565
|
-
while total_written < data_len:
|
566
|
-
rc, bytes_written = write_func(data[total_written:])
|
567
|
-
total_written += bytes_written
|
568
|
-
if rc == eagain:
|
569
|
-
self.poll()
|
570
|
-
sleep()
|
571
|
-
|
572
|
-
def _eagain_errcode(self, func, eagain, *args, **kwargs):
|
573
|
-
timeout = kwargs.pop('timeout', self.timeout)
|
574
|
-
with GTimeout(seconds=timeout, exception=Timeout):
|
575
|
-
ret = func(*args, **kwargs)
|
576
|
-
while ret == eagain:
|
577
|
-
self.poll()
|
578
|
-
ret = func(*args, **kwargs)
|
579
|
-
sleep()
|
580
|
-
return ret
|
581
|
-
|
582
|
-
def _eagain_write(self, write_func, data):
|
583
|
-
raise NotImplementedError
|
584
|
-
|
585
|
-
def _eagain(self, func, *args, **kwargs):
|
586
|
-
raise NotImplementedError
|
587
|
-
|
588
635
|
def _make_sftp(self):
|
589
636
|
raise NotImplementedError
|
590
637
|
|
@@ -692,24 +739,3 @@ class BaseSSHClient(object):
|
|
692
739
|
_sep = file_path.rfind('/')
|
693
740
|
if _sep > 0:
|
694
741
|
return file_path[:_sep]
|
695
|
-
|
696
|
-
def poll(self):
|
697
|
-
raise NotImplementedError
|
698
|
-
|
699
|
-
def _poll_socket(self, events):
|
700
|
-
if self.sock is None:
|
701
|
-
return
|
702
|
-
poller = poll()
|
703
|
-
poller.register(self.sock, eventmask=events)
|
704
|
-
poller.poll(timeout=1)
|
705
|
-
|
706
|
-
def _poll_errcodes(self, directions_func, inbound, outbound):
|
707
|
-
directions = directions_func()
|
708
|
-
if directions == 0:
|
709
|
-
return
|
710
|
-
events = 0
|
711
|
-
if directions & inbound:
|
712
|
-
events = POLLIN
|
713
|
-
if directions & outbound:
|
714
|
-
events |= POLLOUT
|
715
|
-
self._poll_socket(events)
|
pssh/clients/native/single.py
CHANGED
@@ -15,12 +15,15 @@
|
|
15
15
|
# License along with this library; if not, write to the Free Software
|
16
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
|
@@ -31,7 +34,7 @@ from ssh2.sftp import LIBSSH2_FXF_READ, LIBSSH2_FXF_CREAT, LIBSSH2_FXF_WRITE, \
|
|
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,7 +106,8 @@ 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,
|
64
113
|
):
|
@@ -85,7 +134,9 @@ class SSHClient(BaseSSHClient):
|
|
85
134
|
to :py:class:`pssh.constants.RETRY_DELAY`
|
86
135
|
:type retry_delay: int or float
|
87
136
|
:param timeout: SSH session timeout setting in seconds. This controls
|
88
|
-
timeout setting of authenticated SSH sessions.
|
137
|
+
timeout setting of authenticated SSH sessions for each individual SSH operation.
|
138
|
+
Also currently sets socket as well as per function timeout in some cases, see
|
139
|
+
function descriptions.
|
89
140
|
:type timeout: int or float
|
90
141
|
:param allow_agent: (Optional) set to False to disable connecting to
|
91
142
|
the system's SSH agent
|
@@ -146,7 +197,7 @@ class SSHClient(BaseSSHClient):
|
|
146
197
|
)
|
147
198
|
|
148
199
|
def _shell(self, channel):
|
149
|
-
return self.
|
200
|
+
return self.eagain(channel.shell)
|
150
201
|
|
151
202
|
def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
|
152
203
|
user=None, password=None, alias=None,
|
@@ -178,42 +229,36 @@ class SSHClient(BaseSSHClient):
|
|
178
229
|
proxy_local_port = FORWARDER.out_q.get()
|
179
230
|
return proxy_local_port
|
180
231
|
|
181
|
-
def
|
232
|
+
def _disconnect(self):
|
182
233
|
"""Attempt to disconnect session.
|
183
234
|
|
184
235
|
Any errors on calling disconnect are suppressed by this function.
|
236
|
+
|
237
|
+
Does not need to be called directly - called when client object is de-allocated.
|
185
238
|
"""
|
186
239
|
self._keepalive_greenlet = None
|
187
|
-
if self.session is not None:
|
240
|
+
if self.session is not None and self.sock is not None and not self.sock.closed:
|
188
241
|
try:
|
189
242
|
self._disconnect_eagain()
|
190
243
|
except Exception:
|
191
244
|
pass
|
192
|
-
|
245
|
+
self.session = None
|
246
|
+
# To allow for file descriptor reuse, which is part of gevent, shutdown but do not close socket here.
|
247
|
+
# Done by gevent when file descriptor is closed.
|
248
|
+
if self.sock is not None and not self.sock.closed:
|
249
|
+
try:
|
250
|
+
self.sock.shutdown(SHUT_RDWR)
|
251
|
+
self.sock.detach()
|
252
|
+
except Exception:
|
253
|
+
pass
|
254
|
+
self.sock = None
|
255
|
+
# Notify forwarder that proxy tunnel server can be shutdown
|
193
256
|
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
257
|
FORWARDER.cleanup_server(self._proxy_client)
|
198
258
|
|
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
259
|
def configure_keepalive(self):
|
216
260
|
"""Configures keepalive on the server for `self.keepalive_seconds`."""
|
261
|
+
# Configure keepalives without a reply.
|
217
262
|
self.session.keepalive_config(False, self.keepalive_seconds)
|
218
263
|
|
219
264
|
def _init_session(self, retries=1):
|
@@ -234,7 +279,11 @@ class SSHClient(BaseSSHClient):
|
|
234
279
|
msg = "Error connecting to host %s:%s - %s"
|
235
280
|
logger.error(msg, self.host, self.port, ex)
|
236
281
|
if not self.sock.closed:
|
237
|
-
|
282
|
+
try:
|
283
|
+
self.sock.shutdown(SHUT_RDWR)
|
284
|
+
self.sock.detach()
|
285
|
+
except Exception:
|
286
|
+
pass
|
238
287
|
if isinstance(ex, SSH2Timeout):
|
239
288
|
raise Timeout(msg, self.host, self.port, ex)
|
240
289
|
raise
|
@@ -242,7 +291,7 @@ class SSHClient(BaseSSHClient):
|
|
242
291
|
def _keepalive(self):
|
243
292
|
if self.keepalive_seconds:
|
244
293
|
self.configure_keepalive()
|
245
|
-
self._keepalive_greenlet = self.
|
294
|
+
self._keepalive_greenlet = KeepAlive(self.sock, self.session)
|
246
295
|
|
247
296
|
def _agent_auth(self):
|
248
297
|
self.session.agent_auth(self.user)
|
@@ -264,7 +313,7 @@ class SSHClient(BaseSSHClient):
|
|
264
313
|
self.session.userauth_password(self.user, self.password)
|
265
314
|
|
266
315
|
def _open_session(self):
|
267
|
-
chan = self.
|
316
|
+
chan = self.eagain(self.session.open_session)
|
268
317
|
return chan
|
269
318
|
|
270
319
|
def open_session(self):
|
@@ -282,14 +331,18 @@ class SSHClient(BaseSSHClient):
|
|
282
331
|
return chan
|
283
332
|
|
284
333
|
def _make_output_readers(self, channel, stdout_buffer, stderr_buffer):
|
285
|
-
|
334
|
+
# TODO: These greenlets need to be outside client scope or we create a reader <-> client cyclical reference
|
335
|
+
_stdout_reader = self._pool.spawn(
|
286
336
|
self._read_output_to_buffer, channel.read, stdout_buffer)
|
287
|
-
_stderr_reader = spawn(
|
337
|
+
_stderr_reader = self._pool.spawn(
|
288
338
|
self._read_output_to_buffer, channel.read_stderr, stderr_buffer)
|
289
339
|
return _stdout_reader, _stderr_reader
|
290
340
|
|
291
|
-
def
|
292
|
-
"""
|
341
|
+
def _execute(self, cmd, use_pty=False, channel=None):
|
342
|
+
"""
|
343
|
+
Use ``run_command`` which returns a ``HostOutput`` object rather than this function directly.
|
344
|
+
|
345
|
+
Execute command on remote server.
|
293
346
|
|
294
347
|
:param cmd: Command to execute.
|
295
348
|
:type cmd: str
|
@@ -298,12 +351,14 @@ class SSHClient(BaseSSHClient):
|
|
298
351
|
:param channel: Use provided channel for execute rather than creating
|
299
352
|
a new one.
|
300
353
|
:type channel: :py:class:`ssh2.channel.Channel`
|
354
|
+
|
355
|
+
:rtype: :py:class:`ssh2.channel.Channel`
|
301
356
|
"""
|
302
357
|
channel = self.open_session() if channel is None else channel
|
303
358
|
if use_pty:
|
304
|
-
self.
|
359
|
+
self.eagain(channel.pty)
|
305
360
|
logger.debug("Executing command '%s'", cmd)
|
306
|
-
self.
|
361
|
+
self.eagain(channel.execute, cmd)
|
307
362
|
return channel
|
308
363
|
|
309
364
|
def _read_output_to_buffer(self, read_func, _buffer, is_stderr=False):
|
@@ -341,20 +396,23 @@ class SSHClient(BaseSSHClient):
|
|
341
396
|
channel = host_output.channel
|
342
397
|
if channel is None:
|
343
398
|
return
|
344
|
-
|
345
|
-
|
346
|
-
|
399
|
+
with GTimeout(seconds=timeout, exception=Timeout):
|
400
|
+
self.eagain(channel.wait_eof)
|
401
|
+
# Close channel to indicate no more commands will be sent over it
|
402
|
+
self.close_channel(channel)
|
347
403
|
|
348
404
|
def close_channel(self, channel):
|
405
|
+
"""Close given channel, handling EAGAIN."""
|
349
406
|
with self._chan_stdout_lock, self._chan_stderr_lock:
|
350
407
|
logger.debug("Closing channel")
|
351
|
-
self.
|
408
|
+
self.eagain(channel.close)
|
352
409
|
|
353
|
-
def
|
410
|
+
def eagain(self, func, *args, **kwargs):
|
411
|
+
"""Handle EAGAIN and call given function with any args, polling for as long as there is data to receive."""
|
354
412
|
return self._eagain_errcode(func, LIBSSH2_ERROR_EAGAIN, *args, **kwargs)
|
355
413
|
|
356
414
|
def _make_sftp_eagain(self):
|
357
|
-
return self.
|
415
|
+
return self.eagain(self.session.sftp_init)
|
358
416
|
|
359
417
|
def _make_sftp(self):
|
360
418
|
try:
|
@@ -381,7 +439,7 @@ class SSHClient(BaseSSHClient):
|
|
381
439
|
LIBSSH2_SFTP_S_IXGRP | \
|
382
440
|
LIBSSH2_SFTP_S_IXOTH
|
383
441
|
try:
|
384
|
-
self.
|
442
|
+
self.eagain(sftp.mkdir, directory, mode)
|
385
443
|
except SFTPProtocolError as error:
|
386
444
|
msg = "Error occured creating directory %s on host %s - %s"
|
387
445
|
logger.error(msg, directory, self.host, error)
|
@@ -419,7 +477,7 @@ class SSHClient(BaseSSHClient):
|
|
419
477
|
destination = self._remote_paths_split(remote_file)
|
420
478
|
if destination is not None:
|
421
479
|
try:
|
422
|
-
self.
|
480
|
+
self.eagain(sftp.stat, destination)
|
423
481
|
except (SFTPHandleError, SFTPProtocolError):
|
424
482
|
self.mkdir(sftp, destination)
|
425
483
|
self.sftp_put(sftp, local_file, remote_file)
|
@@ -482,7 +540,7 @@ class SSHClient(BaseSSHClient):
|
|
482
540
|
cur_dir = _paths_to_create.popleft()
|
483
541
|
cwd = '/'.join([cwd, cur_dir])
|
484
542
|
try:
|
485
|
-
self.
|
543
|
+
self.eagain(sftp.stat, cwd)
|
486
544
|
except (SFTPHandleError, SFTPProtocolError) as ex:
|
487
545
|
logger.debug("Stat for %s failed with %s", cwd, ex)
|
488
546
|
self._mkdir(sftp, cwd)
|
@@ -514,7 +572,7 @@ class SSHClient(BaseSSHClient):
|
|
514
572
|
"""
|
515
573
|
sftp = self._make_sftp() if sftp is None else sftp
|
516
574
|
try:
|
517
|
-
self.
|
575
|
+
self.eagain(sftp.stat, remote_file)
|
518
576
|
except (SFTPHandleError, SFTPProtocolError):
|
519
577
|
msg = "Remote file or directory %s on host %s does not exist"
|
520
578
|
logger.error(msg, remote_file, self.host)
|
@@ -541,7 +599,7 @@ class SSHClient(BaseSSHClient):
|
|
541
599
|
|
542
600
|
def _scp_recv_recursive(self, remote_file, local_file, sftp, encoding='utf-8'):
|
543
601
|
try:
|
544
|
-
self.
|
602
|
+
self.eagain(sftp.stat, remote_file)
|
545
603
|
except (SFTPHandleError, SFTPProtocolError):
|
546
604
|
msg = "Remote file or directory %s does not exist"
|
547
605
|
logger.error(msg, remote_file)
|
@@ -602,7 +660,7 @@ class SSHClient(BaseSSHClient):
|
|
602
660
|
|
603
661
|
def _scp_recv(self, remote_file, local_file):
|
604
662
|
try:
|
605
|
-
(file_chan, fileinfo) = self.
|
663
|
+
(file_chan, fileinfo) = self.eagain(
|
606
664
|
self.session.scp_recv2, remote_file)
|
607
665
|
except Exception as ex:
|
608
666
|
msg = "Error copying file %s from host %s - %s"
|
@@ -661,7 +719,7 @@ class SSHClient(BaseSSHClient):
|
|
661
719
|
if destination is not None:
|
662
720
|
sftp = self._make_sftp() if sftp is None else sftp
|
663
721
|
try:
|
664
|
-
self.
|
722
|
+
self.eagain(sftp.stat, destination)
|
665
723
|
except (SFTPHandleError, SFTPProtocolError):
|
666
724
|
self.mkdir(sftp, destination)
|
667
725
|
elif remote_file.endswith('/'):
|
@@ -674,7 +732,7 @@ class SSHClient(BaseSSHClient):
|
|
674
732
|
def _scp_send(self, local_file, remote_file):
|
675
733
|
fileinfo = os.stat(local_file)
|
676
734
|
try:
|
677
|
-
chan = self.
|
735
|
+
chan = self.eagain(
|
678
736
|
self.session.scp_send64,
|
679
737
|
remote_file, fileinfo.st_mode & 0o777, fileinfo.st_size,
|
680
738
|
fileinfo.st_mtime, fileinfo.st_atime)
|
@@ -693,14 +751,14 @@ class SSHClient(BaseSSHClient):
|
|
693
751
|
logger.error(msg, remote_file, self.host, ex)
|
694
752
|
raise SCPError(msg, remote_file, self.host, ex)
|
695
753
|
finally:
|
696
|
-
self.
|
697
|
-
self.
|
698
|
-
self.
|
699
|
-
self.
|
754
|
+
self.eagain(chan.flush)
|
755
|
+
self.eagain(chan.send_eof)
|
756
|
+
self.eagain(chan.wait_eof)
|
757
|
+
self.eagain(chan.wait_closed)
|
700
758
|
|
701
759
|
def _sftp_openfh(self, open_func, remote_file, *args):
|
702
760
|
try:
|
703
|
-
fh = self.
|
761
|
+
fh = self.eagain(open_func, remote_file, *args)
|
704
762
|
except Exception as ex:
|
705
763
|
raise SFTPError(ex)
|
706
764
|
return fh
|
@@ -758,12 +816,9 @@ class SSHClient(BaseSSHClient):
|
|
758
816
|
LIBSSH2_SESSION_BLOCK_OUTBOUND,
|
759
817
|
)
|
760
818
|
|
761
|
-
def
|
819
|
+
def eagain_write(self, write_func, data):
|
762
820
|
"""Write data with given write_func for an ssh2-python session while
|
763
821
|
handling EAGAIN and resuming writes from last written byte on each call to
|
764
822
|
write_func.
|
765
823
|
"""
|
766
824
|
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)
|
pssh/clients/native/tunnel.py
CHANGED
@@ -15,13 +15,14 @@
|
|
15
15
|
# License along with this library; if not, write to the Free Software
|
16
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
17
17
|
|
18
|
+
import atexit
|
18
19
|
import logging
|
19
|
-
from
|
20
|
-
from
|
21
|
-
|
22
|
-
from gevent import spawn, joinall, get_hub, sleep
|
20
|
+
from gevent import joinall, get_hub, sleep
|
21
|
+
from gevent.pool import Pool
|
23
22
|
from gevent.server import StreamServer
|
23
|
+
from queue import Queue
|
24
24
|
from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
|
25
|
+
from threading import Thread, Event
|
25
26
|
|
26
27
|
from ...constants import DEFAULT_RETRIES
|
27
28
|
|
@@ -39,25 +40,39 @@ class LocalForwarder(Thread):
|
|
39
40
|
and get port to connect to via ``out_q`` once a target has been put into the input queue.
|
40
41
|
|
41
42
|
``SSHClient`` is the client for the SSH host that will be proxying.
|
43
|
+
|
44
|
+
There can be as many LocalForwarder(s) as needed to scale.
|
45
|
+
|
46
|
+
Relationship of clients to forwarders to servers is:
|
47
|
+
|
48
|
+
One client -> One Forwarder -> Many Servers <-> Many proxy hosts -> one target host per proxy host
|
49
|
+
|
50
|
+
A Forwarder starts servers. Servers communicate with clients directly via Forwarder thread.
|
51
|
+
|
52
|
+
Multiple forwarder threads can be used to scale clients to more threads as number of clients increases causing
|
53
|
+
contention in forwarder threads handling proxy connections.
|
42
54
|
"""
|
43
|
-
Thread.__init__(self)
|
55
|
+
Thread.__init__(self, daemon=True)
|
44
56
|
self.in_q = Queue(1)
|
45
57
|
self.out_q = Queue(1)
|
58
|
+
self._pool = None
|
46
59
|
self._servers = {}
|
47
60
|
self._hub = None
|
48
61
|
self.started = Event()
|
49
62
|
self._cleanup_let = None
|
63
|
+
self.shutdown_triggered = Event()
|
50
64
|
|
51
65
|
def _start_server(self):
|
52
66
|
client, host, port = self.in_q.get()
|
67
|
+
logger.debug("Starting server for %s:%s", host, port)
|
53
68
|
server = TunnelServer(client, host, port)
|
69
|
+
self._servers[client] = server
|
54
70
|
server.start()
|
55
|
-
self._get_server_listen_port(
|
71
|
+
self._get_server_listen_port(server)
|
56
72
|
|
57
|
-
def _get_server_listen_port(self,
|
73
|
+
def _get_server_listen_port(self, server):
|
58
74
|
while not server.started:
|
59
75
|
sleep(0.01)
|
60
|
-
self._servers[client] = server
|
61
76
|
local_port = server.listen_port
|
62
77
|
self.out_q.put(local_port)
|
63
78
|
|
@@ -74,9 +89,20 @@ class LocalForwarder(Thread):
|
|
74
89
|
self.in_q.put((client, host, port))
|
75
90
|
|
76
91
|
def shutdown(self):
|
77
|
-
"""Stop all tunnel servers.
|
92
|
+
"""Stop all tunnel servers and shutdown LocalForwarder thread.
|
93
|
+
|
94
|
+
This function will join the current thread and wait for it to shutdown if needed.
|
95
|
+
"""
|
78
96
|
for client, server in self._servers.items():
|
79
97
|
server.stop()
|
98
|
+
self._servers = {}
|
99
|
+
if self.started:
|
100
|
+
self.shutdown_triggered.set()
|
101
|
+
try:
|
102
|
+
self.join()
|
103
|
+
except RuntimeError:
|
104
|
+
# Re-entry protection
|
105
|
+
pass
|
80
106
|
|
81
107
|
def _cleanup_servers_let(self):
|
82
108
|
while True:
|
@@ -89,19 +115,22 @@ class LocalForwarder(Thread):
|
|
89
115
|
self.cleanup_server(client)
|
90
116
|
|
91
117
|
def run(self):
|
92
|
-
"""Thread runner ensures a non
|
118
|
+
"""Thread runner ensures a non-main hub has been created for all subsequent
|
93
119
|
greenlets and waits for (client, host, port) tuples to be put into self.in_q.
|
94
120
|
|
95
|
-
A server is created once something is in the queue and the port to connect to
|
96
|
-
|
121
|
+
A server is created once something is in the queue and only then is the port to connect to
|
122
|
+
put into self.out_q.
|
97
123
|
"""
|
98
124
|
self._hub = get_hub()
|
99
125
|
assert self._hub.main_hub is False
|
126
|
+
self._pool = Pool(1)
|
100
127
|
self.started.set()
|
101
|
-
self._cleanup_let = spawn(self._cleanup_servers_let)
|
128
|
+
self._cleanup_let = self._pool.spawn(self._cleanup_servers_let)
|
102
129
|
logger.debug("Hub in server runner is main hub: %s", self._hub.main_hub)
|
103
130
|
try:
|
104
131
|
while True:
|
132
|
+
if self.shutdown_triggered.is_set():
|
133
|
+
return
|
105
134
|
if self.in_q.empty():
|
106
135
|
sleep(.01)
|
107
136
|
continue
|
@@ -111,8 +140,9 @@ class LocalForwarder(Thread):
|
|
111
140
|
self.shutdown()
|
112
141
|
|
113
142
|
def cleanup_server(self, client):
|
114
|
-
"""
|
115
|
-
|
143
|
+
"""
|
144
|
+
Stop server for given proxy client and remove client from this forwarder.
|
145
|
+
"""
|
116
146
|
server = self._servers[client]
|
117
147
|
server.stop()
|
118
148
|
del self._servers[client]
|
@@ -123,16 +153,18 @@ class TunnelServer(StreamServer):
|
|
123
153
|
|
124
154
|
Accepts connections on an available bind_address port once started and tunnels data
|
125
155
|
to/from remote SSH host for each connection.
|
156
|
+
|
157
|
+
TunnelServer.listen_port will return listening port for server on given host once TunnelServer.started is True.
|
126
158
|
"""
|
127
159
|
|
128
160
|
def __init__(self, client, host, port, bind_address='127.0.0.1',
|
129
161
|
num_retries=DEFAULT_RETRIES):
|
130
|
-
StreamServer.__init__(self, (bind_address, 0), self._read_rw)
|
131
162
|
self.client = client
|
163
|
+
self._pool = Pool()
|
164
|
+
StreamServer.__init__(self, (bind_address, 0), self._read_rw, spawn=self._pool)
|
132
165
|
self.host = host
|
133
166
|
self.port = port
|
134
167
|
self.session = client.session
|
135
|
-
self._client = client
|
136
168
|
self._retries = num_retries
|
137
169
|
self.bind_address = bind_address
|
138
170
|
self.exception = None
|
@@ -154,41 +186,40 @@ class TunnelServer(StreamServer):
|
|
154
186
|
self.host, self.port, ex)
|
155
187
|
self.exception = ex
|
156
188
|
return
|
157
|
-
|
158
|
-
|
189
|
+
# Channel remains alive while this handler function is alive
|
190
|
+
source = self._pool.spawn(self._read_forward_sock, socket, channel)
|
191
|
+
dest = self._pool.spawn(self._read_channel, socket, channel)
|
159
192
|
logger.debug("Waiting for read/write greenlets")
|
160
|
-
self._source_let = source
|
161
|
-
self._dest_let = dest
|
162
193
|
self._wait_send_receive_lets(source, dest, channel)
|
163
194
|
|
164
195
|
def _wait_send_receive_lets(self, source, dest, channel):
|
165
196
|
try:
|
166
197
|
joinall((source, dest), raise_error=True)
|
167
198
|
finally:
|
168
|
-
|
169
|
-
|
170
|
-
self.
|
171
|
-
|
172
|
-
# Disconnect client here to make sure it happens AFTER close_channel
|
173
|
-
self._client.disconnect()
|
199
|
+
logger.debug("Read/Write greenlets for tunnel target %s:%s finished, closing forwarding channel",
|
200
|
+
self.host, self.port)
|
201
|
+
self.client.close_channel(channel)
|
174
202
|
|
175
203
|
def _read_forward_sock(self, forward_sock, channel):
|
176
204
|
while True:
|
177
205
|
if channel is None or channel.eof():
|
178
206
|
logger.debug("Channel closed, tunnel forward socket reader exiting")
|
179
207
|
return
|
208
|
+
if not forward_sock or forward_sock.closed:
|
209
|
+
logger.debug("Forward socket closed, tunnel forward socket reader exiting")
|
210
|
+
return
|
180
211
|
try:
|
181
212
|
data = forward_sock.recv(1024)
|
182
213
|
except Exception as ex:
|
183
214
|
logger.error("Forward socket read error: %s", ex)
|
184
215
|
raise
|
185
216
|
data_len = len(data)
|
186
|
-
# logger.debug("Read %s data from forward socket", data_len
|
217
|
+
# logger.debug("Read %s data from forward socket", data_len)
|
187
218
|
if data_len == 0:
|
188
219
|
sleep(.01)
|
189
220
|
continue
|
190
221
|
try:
|
191
|
-
self.
|
222
|
+
self.client.eagain_write(channel.write, data)
|
192
223
|
except Exception as ex:
|
193
224
|
logger.error("Error writing data to channel - %s", ex)
|
194
225
|
raise
|
@@ -204,9 +235,9 @@ class TunnelServer(StreamServer):
|
|
204
235
|
except Exception as ex:
|
205
236
|
logger.error("Error reading from channel - %s", ex)
|
206
237
|
raise
|
207
|
-
# logger.debug("Read %s data from channel"
|
238
|
+
# logger.debug("Read %s data from channel", size)
|
208
239
|
if size == LIBSSH2_ERROR_EAGAIN:
|
209
|
-
self.
|
240
|
+
self.client.poll()
|
210
241
|
continue
|
211
242
|
elif size == 0:
|
212
243
|
sleep(.01)
|
@@ -224,7 +255,7 @@ class TunnelServer(StreamServer):
|
|
224
255
|
fw_host, fw_port, self.bind_address,
|
225
256
|
local_port)
|
226
257
|
while channel == LIBSSH2_ERROR_EAGAIN:
|
227
|
-
self.
|
258
|
+
self.client.poll()
|
228
259
|
channel = self.session.direct_tcpip_ex(
|
229
260
|
fw_host, fw_port, self.bind_address,
|
230
261
|
local_port)
|
@@ -249,4 +280,4 @@ class TunnelServer(StreamServer):
|
|
249
280
|
|
250
281
|
|
251
282
|
FORWARDER = LocalForwarder()
|
252
|
-
FORWARDER.
|
283
|
+
atexit.register(FORWARDER.shutdown)
|
pssh/clients/ssh/single.py
CHANGED
@@ -16,8 +16,8 @@
|
|
16
16
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
17
17
|
|
18
18
|
import logging
|
19
|
-
|
20
19
|
from gevent import sleep, spawn, Timeout as GTimeout, joinall
|
20
|
+
from gevent.socket import SHUT_RDWR
|
21
21
|
from ssh import options
|
22
22
|
from ssh.error_codes import SSH_AGAIN
|
23
23
|
from ssh.exceptions import EOF
|
@@ -81,9 +81,11 @@ class SSHClient(BaseSSHClient):
|
|
81
81
|
:type retry_delay: int or float
|
82
82
|
:param timeout: (Optional) If provided, all commands will timeout after
|
83
83
|
<timeout> number of seconds.
|
84
|
+
Also currently sets socket as well as per function timeout in some cases, see
|
85
|
+
function descriptions.
|
84
86
|
:type timeout: int or float
|
85
87
|
:param allow_agent: (Optional) set to False to disable connecting to
|
86
|
-
the system's SSH agent.
|
88
|
+
the system's SSH agent.
|
87
89
|
:type allow_agent: bool
|
88
90
|
:param identity_auth: (Optional) set to False to disable attempting to
|
89
91
|
authenticate with default identity files from
|
@@ -124,10 +126,18 @@ class SSHClient(BaseSSHClient):
|
|
124
126
|
ipv6_only=ipv6_only,
|
125
127
|
)
|
126
128
|
|
127
|
-
def
|
128
|
-
"""
|
129
|
-
|
130
|
-
|
129
|
+
def _disconnect(self):
|
130
|
+
"""Shutdown socket if needed.
|
131
|
+
|
132
|
+
Does not need to be called directly - called when client object is de-allocated.
|
133
|
+
"""
|
134
|
+
if self.session is not None and self.sock is not None and not self.sock.closed:
|
135
|
+
try:
|
136
|
+
self.sock.shutdown(SHUT_RDWR)
|
137
|
+
self.sock.detach()
|
138
|
+
except Exception:
|
139
|
+
pass
|
140
|
+
self.sock = None
|
131
141
|
|
132
142
|
def _agent_auth(self):
|
133
143
|
self.session.userauth_agent(self.user)
|
@@ -201,12 +211,12 @@ class SSHClient(BaseSSHClient):
|
|
201
211
|
logger.debug("Imported certificate file %s for pkey %s", self.cert_file, self.pkey)
|
202
212
|
|
203
213
|
def _shell(self, channel):
|
204
|
-
return self.
|
214
|
+
return self.eagain(channel.request_shell)
|
205
215
|
|
206
216
|
def _open_session(self):
|
207
217
|
channel = self.session.channel_new()
|
208
218
|
channel.set_blocking(0)
|
209
|
-
self.
|
219
|
+
self.eagain(channel.open_session)
|
210
220
|
return channel
|
211
221
|
|
212
222
|
def open_session(self):
|
@@ -225,7 +235,7 @@ class SSHClient(BaseSSHClient):
|
|
225
235
|
self._read_output_to_buffer, channel, stderr_buffer, is_stderr=True)
|
226
236
|
return _stdout_reader, _stderr_reader
|
227
237
|
|
228
|
-
def
|
238
|
+
def _execute(self, cmd, use_pty=False, channel=None):
|
229
239
|
"""Execute command on remote host.
|
230
240
|
|
231
241
|
:param cmd: The command string to execute.
|
@@ -235,12 +245,13 @@ class SSHClient(BaseSSHClient):
|
|
235
245
|
:type use_pty: bool
|
236
246
|
:param channel: Channel to use. New channel is created if not provided.
|
237
247
|
:type channel: :py:class:`ssh.channel.Channel`"""
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
248
|
+
with GTimeout(seconds=self.timeout, exception=Timeout):
|
249
|
+
channel = self.open_session() if not channel else channel
|
250
|
+
if use_pty:
|
251
|
+
self.eagain(channel.request_pty)
|
252
|
+
logger.debug("Executing command '%s'", cmd)
|
253
|
+
self.eagain(channel.request_exec, cmd)
|
254
|
+
return channel
|
244
255
|
|
245
256
|
def _read_output_to_buffer(self, channel, _buffer, is_stderr=False):
|
246
257
|
try:
|
@@ -276,7 +287,7 @@ class SSHClient(BaseSSHClient):
|
|
276
287
|
if channel is None:
|
277
288
|
return
|
278
289
|
logger.debug("Sending EOF on channel %s", channel)
|
279
|
-
self.
|
290
|
+
self.eagain(channel.send_eof)
|
280
291
|
logger.debug("Waiting for readers, timeout %s", timeout)
|
281
292
|
with GTimeout(seconds=timeout, exception=Timeout):
|
282
293
|
joinall((host_output.buffers.stdout.reader, host_output.buffers.stderr.reader))
|
@@ -311,7 +322,7 @@ class SSHClient(BaseSSHClient):
|
|
311
322
|
:type channel: :py:class:`ssh.channel.Channel`
|
312
323
|
"""
|
313
324
|
logger.debug("Closing channel")
|
314
|
-
self.
|
325
|
+
self.eagain(channel.close)
|
315
326
|
|
316
327
|
def poll(self, timeout=None):
|
317
328
|
"""ssh-python based co-operative gevent poll on session socket.
|
@@ -323,9 +334,9 @@ class SSHClient(BaseSSHClient):
|
|
323
334
|
SSH_WRITE_PENDING,
|
324
335
|
)
|
325
336
|
|
326
|
-
def
|
337
|
+
def eagain(self, func, *args, **kwargs):
|
327
338
|
"""Run function given and handle EAGAIN for an ssh-python session"""
|
328
339
|
return self._eagain_errcode(func, SSH_AGAIN, *args, **kwargs)
|
329
340
|
|
330
|
-
def
|
341
|
+
def eagain_write(self, write_func, data):
|
331
342
|
return self._eagain_write_errcode(write_func, data, SSH_AGAIN)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|