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/tunnel.py
CHANGED
@@ -1,27 +1,28 @@
|
|
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 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/reader.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
|
from io import BytesIO
|
19
19
|
|
pssh/clients/ssh/__init__.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
|
# flake8: noqa: F401
|
19
19
|
from .parallel import ParallelSSHClient
|
pssh/clients/ssh/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
|
|
@@ -40,6 +40,7 @@ class ParallelSSHClient(BaseParallelSSHClient):
|
|
40
40
|
gssapi_delegate_credentials=False,
|
41
41
|
identity_auth=True,
|
42
42
|
ipv6_only=False,
|
43
|
+
compress=False,
|
43
44
|
):
|
44
45
|
"""
|
45
46
|
:param hosts: Hosts to connect to
|
@@ -114,6 +115,8 @@ class ParallelSSHClient(BaseParallelSSHClient):
|
|
114
115
|
for the host or raise NoIPv6AddressFoundError otherwise. Note this will
|
115
116
|
disable connecting to an IPv4 address if an IP address is provided instead.
|
116
117
|
:type ipv6_only: bool
|
118
|
+
:param compress: Enable/Disable compression on the client. Defaults to off.
|
119
|
+
:type compress: bool
|
117
120
|
|
118
121
|
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
|
119
122
|
provided private key.
|
@@ -125,6 +128,7 @@ class ParallelSSHClient(BaseParallelSSHClient):
|
|
125
128
|
host_config=host_config, retry_delay=retry_delay,
|
126
129
|
identity_auth=identity_auth,
|
127
130
|
ipv6_only=ipv6_only,
|
131
|
+
compress=compress,
|
128
132
|
)
|
129
133
|
self.pkey = _validate_pkey(pkey)
|
130
134
|
self.cert_file = _validate_pkey_path(cert_file)
|
@@ -228,5 +232,6 @@ class ParallelSSHClient(BaseParallelSSHClient):
|
|
228
232
|
gssapi_client_identity=self.gssapi_client_identity,
|
229
233
|
gssapi_delegate_credentials=self.gssapi_delegate_credentials,
|
230
234
|
cert_file=cfg.cert_file,
|
235
|
+
compress=cfg.compress or self.compress,
|
231
236
|
)
|
232
237
|
return _client
|
pssh/clients/ssh/single.py
CHANGED
@@ -1,23 +1,23 @@
|
|
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
|
-
|
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
|
@@ -50,6 +50,7 @@ class SSHClient(BaseSSHClient):
|
|
50
50
|
gssapi_client_identity=None,
|
51
51
|
gssapi_delegate_credentials=False,
|
52
52
|
ipv6_only=False,
|
53
|
+
compress=False,
|
53
54
|
_auth_thread_pool=True):
|
54
55
|
""":param host: Host name or IP to connect to.
|
55
56
|
:type host: str
|
@@ -81,9 +82,11 @@ class SSHClient(BaseSSHClient):
|
|
81
82
|
:type retry_delay: int or float
|
82
83
|
:param timeout: (Optional) If provided, all commands will timeout after
|
83
84
|
<timeout> number of seconds.
|
85
|
+
Also currently sets socket as well as per function timeout in some cases, see
|
86
|
+
function descriptions.
|
84
87
|
:type timeout: int or float
|
85
88
|
:param allow_agent: (Optional) set to False to disable connecting to
|
86
|
-
the system's SSH agent.
|
89
|
+
the system's SSH agent.
|
87
90
|
:type allow_agent: bool
|
88
91
|
:param identity_auth: (Optional) set to False to disable attempting to
|
89
92
|
authenticate with default identity files from
|
@@ -105,6 +108,8 @@ class SSHClient(BaseSSHClient):
|
|
105
108
|
for the host or raise NoIPv6AddressFoundError otherwise. Note this will
|
106
109
|
disable connecting to an IPv4 address if an IP address is provided instead.
|
107
110
|
:type ipv6_only: bool
|
111
|
+
:param compress: Enable/Disable compression on the client. Defaults to off.
|
112
|
+
:type compress: bool
|
108
113
|
|
109
114
|
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
|
110
115
|
provided private key.
|
@@ -122,12 +127,21 @@ class SSHClient(BaseSSHClient):
|
|
122
127
|
timeout=timeout,
|
123
128
|
identity_auth=identity_auth,
|
124
129
|
ipv6_only=ipv6_only,
|
130
|
+
compress=compress,
|
125
131
|
)
|
126
132
|
|
127
|
-
def
|
128
|
-
"""
|
129
|
-
|
130
|
-
|
133
|
+
def _disconnect(self):
|
134
|
+
"""Shutdown socket if needed.
|
135
|
+
|
136
|
+
Does not need to be called directly - called when client object is de-allocated.
|
137
|
+
"""
|
138
|
+
if self.session is not None and self.sock is not None and not self.sock.closed:
|
139
|
+
try:
|
140
|
+
self.sock.shutdown(SHUT_RDWR)
|
141
|
+
self.sock.detach()
|
142
|
+
except Exception:
|
143
|
+
pass
|
144
|
+
self.sock = None
|
131
145
|
|
132
146
|
def _agent_auth(self):
|
133
147
|
self.session.userauth_agent(self.user)
|
@@ -141,6 +155,8 @@ class SSHClient(BaseSSHClient):
|
|
141
155
|
self.session = Session()
|
142
156
|
self.session.options_set(options.USER, self.user)
|
143
157
|
self.session.options_set(options.HOST, self.host)
|
158
|
+
if self.compress:
|
159
|
+
self.session.options_set(options.COMPRESSION, "yes")
|
144
160
|
self.session.options_set_port(self.port)
|
145
161
|
if self.gssapi_server_identity:
|
146
162
|
self.session.options_set(
|
@@ -201,12 +217,12 @@ class SSHClient(BaseSSHClient):
|
|
201
217
|
logger.debug("Imported certificate file %s for pkey %s", self.cert_file, self.pkey)
|
202
218
|
|
203
219
|
def _shell(self, channel):
|
204
|
-
return self.
|
220
|
+
return self.eagain(channel.request_shell)
|
205
221
|
|
206
222
|
def _open_session(self):
|
207
223
|
channel = self.session.channel_new()
|
208
224
|
channel.set_blocking(0)
|
209
|
-
self.
|
225
|
+
self.eagain(channel.open_session)
|
210
226
|
return channel
|
211
227
|
|
212
228
|
def open_session(self):
|
@@ -225,7 +241,7 @@ class SSHClient(BaseSSHClient):
|
|
225
241
|
self._read_output_to_buffer, channel, stderr_buffer, is_stderr=True)
|
226
242
|
return _stdout_reader, _stderr_reader
|
227
243
|
|
228
|
-
def
|
244
|
+
def _execute(self, cmd, use_pty=False, channel=None):
|
229
245
|
"""Execute command on remote host.
|
230
246
|
|
231
247
|
:param cmd: The command string to execute.
|
@@ -235,12 +251,13 @@ class SSHClient(BaseSSHClient):
|
|
235
251
|
:type use_pty: bool
|
236
252
|
:param channel: Channel to use. New channel is created if not provided.
|
237
253
|
:type channel: :py:class:`ssh.channel.Channel`"""
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
254
|
+
with GTimeout(seconds=self.timeout, exception=Timeout):
|
255
|
+
channel = self.open_session() if not channel else channel
|
256
|
+
if use_pty:
|
257
|
+
self.eagain(channel.request_pty)
|
258
|
+
logger.debug("Executing command '%s'", cmd)
|
259
|
+
self.eagain(channel.request_exec, cmd)
|
260
|
+
return channel
|
244
261
|
|
245
262
|
def _read_output_to_buffer(self, channel, _buffer, is_stderr=False):
|
246
263
|
try:
|
@@ -276,7 +293,7 @@ class SSHClient(BaseSSHClient):
|
|
276
293
|
if channel is None:
|
277
294
|
return
|
278
295
|
logger.debug("Sending EOF on channel %s", channel)
|
279
|
-
self.
|
296
|
+
self.eagain(channel.send_eof)
|
280
297
|
logger.debug("Waiting for readers, timeout %s", timeout)
|
281
298
|
with GTimeout(seconds=timeout, exception=Timeout):
|
282
299
|
joinall((host_output.buffers.stdout.reader, host_output.buffers.stderr.reader))
|
@@ -311,7 +328,7 @@ class SSHClient(BaseSSHClient):
|
|
311
328
|
:type channel: :py:class:`ssh.channel.Channel`
|
312
329
|
"""
|
313
330
|
logger.debug("Closing channel")
|
314
|
-
self.
|
331
|
+
self.eagain(channel.close)
|
315
332
|
|
316
333
|
def poll(self, timeout=None):
|
317
334
|
"""ssh-python based co-operative gevent poll on session socket.
|
@@ -323,9 +340,9 @@ class SSHClient(BaseSSHClient):
|
|
323
340
|
SSH_WRITE_PENDING,
|
324
341
|
)
|
325
342
|
|
326
|
-
def
|
343
|
+
def eagain(self, func, *args, **kwargs):
|
327
344
|
"""Run function given and handle EAGAIN for an ssh-python session"""
|
328
345
|
return self._eagain_errcode(func, SSH_AGAIN, *args, **kwargs)
|
329
346
|
|
330
|
-
def
|
347
|
+
def eagain_write(self, write_func, data):
|
331
348
|
return self._eagain_write_errcode(write_func, data, SSH_AGAIN)
|
pssh/config.py
CHANGED
@@ -1,23 +1,25 @@
|
|
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
|
|
19
19
|
"""Host specific configuration."""
|
20
20
|
|
21
|
+
from .exceptions import InvalidAPIUseError
|
22
|
+
|
21
23
|
|
22
24
|
class HostConfig(object):
|
23
25
|
"""Host configuration for ParallelSSHClient.
|
@@ -29,7 +31,7 @@ class HostConfig(object):
|
|
29
31
|
'proxy_host', 'proxy_port', 'proxy_user', 'proxy_password', 'proxy_pkey',
|
30
32
|
'keepalive_seconds', 'ipv6_only', 'cert_file', 'auth_thread_pool', 'gssapi_auth',
|
31
33
|
'gssapi_server_identity', 'gssapi_client_identity', 'gssapi_delegate_credentials',
|
32
|
-
'forward_ssh_agent',
|
34
|
+
'forward_ssh_agent', 'compress', 'keyboard_interactive',
|
33
35
|
)
|
34
36
|
|
35
37
|
def __init__(self, user=None, port=None, password=None, private_key=None,
|
@@ -46,6 +48,8 @@ class HostConfig(object):
|
|
46
48
|
gssapi_client_identity=None,
|
47
49
|
gssapi_delegate_credentials=False,
|
48
50
|
forward_ssh_agent=False,
|
51
|
+
compress=False,
|
52
|
+
keyboard_interactive=False,
|
49
53
|
):
|
50
54
|
"""
|
51
55
|
:param user: Username to login as.
|
@@ -99,6 +103,15 @@ class HostConfig(object):
|
|
99
103
|
:param gssapi_delegate_credentials: Enable/disable server credentials
|
100
104
|
delegation. (pssh.clients.ssh only)
|
101
105
|
:type gssapi_delegate_credentials: bool
|
106
|
+
:param compress: Enable/Disable compression on the client. Defaults to off.
|
107
|
+
:type compress: bool
|
108
|
+
:param keyboard_interactive: Enable/Disable keyboard interactive authentication with provided username and
|
109
|
+
password. An `InvalidAPIUse` error is raised when keyboard_interactive is enabled without a provided password.
|
110
|
+
Defaults to off.
|
111
|
+
:type keyboard_interactive: bool
|
112
|
+
|
113
|
+
:raises: :py:class:`pssh.exceptions.InvalidAPIUseError` when `keyboard_interactive=True` with no password
|
114
|
+
provided.
|
102
115
|
"""
|
103
116
|
self.user = user
|
104
117
|
self.port = port
|
@@ -124,6 +137,10 @@ class HostConfig(object):
|
|
124
137
|
self.gssapi_server_identity = gssapi_server_identity
|
125
138
|
self.gssapi_client_identity = gssapi_client_identity
|
126
139
|
self.gssapi_delegate_credentials = gssapi_delegate_credentials
|
140
|
+
self.compress = compress
|
141
|
+
self.keyboard_interactive = keyboard_interactive
|
142
|
+
if self.keyboard_interactive and not self.password:
|
143
|
+
raise InvalidAPIUseError("Keyboard interactive authentication is enabled but no password is provided")
|
127
144
|
self._sanity_checks()
|
128
145
|
|
129
146
|
def _sanity_checks(self):
|
@@ -181,3 +198,7 @@ class HostConfig(object):
|
|
181
198
|
raise ValueError("GSSAPI client identity %s is not a string", self.gssapi_client_identity)
|
182
199
|
if self.gssapi_delegate_credentials is not None and not isinstance(self.gssapi_delegate_credentials, bool):
|
183
200
|
raise ValueError("GSSAPI delegate credentials %s is not a bool", self.gssapi_delegate_credentials)
|
201
|
+
if self.compress is not None and not isinstance(self.compress, bool):
|
202
|
+
raise ValueError("Compress %s is not a bool", self.compress)
|
203
|
+
if self.keyboard_interactive is not None and not isinstance(self.keyboard_interactive, bool):
|
204
|
+
raise ValueError("keyboard_interactive %s is not a bool", self.keyboard_interactive)
|