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