parallel-ssh 2.13.0rc1__py3-none-any.whl → 2.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,28 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
+ import atexit
18
19
  import logging
19
- from queue import Queue
20
- from threading import Thread, Event
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(client, server)
71
+ self._get_server_listen_port(server)
56
72
 
57
- def _get_server_listen_port(self, client, server):
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 main hub has been created for all subsequent
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
- is put into self.out_q.
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
- """The purpose of this function is for a proxied client to notify the LocalForwarder that it
115
- is shutting down and its corresponding server can also be shut down."""
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
- source = spawn(self._read_forward_sock, socket, channel)
158
- dest = spawn(self._read_channel, socket, channel)
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
- # Forward socket does not need to be closed here; StreamServer does it in do_close
169
- logger.debug("Closing channel")
170
- self._client.close_channel(channel)
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._client.eagain_write(channel.write, data)
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" % (size,))
238
+ # logger.debug("Read %s data from channel", size)
208
239
  if size == LIBSSH2_ERROR_EAGAIN:
209
- self._client.poll()
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._client.poll()
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.daemon = True
283
+ atexit.register(FORWARDER.shutdown)
pssh/clients/reader.py CHANGED
@@ -1,19 +1,19 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  from io import BytesIO
19
19
 
@@ -1,19 +1,19 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  # flake8: noqa: F401
19
19
  from .parallel import ParallelSSHClient
@@ -1,19 +1,19 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  import logging
19
19
 
@@ -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
@@ -1,23 +1,23 @@
1
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
  import logging
19
-
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. Currently unused.
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 disconnect(self):
128
- """Close socket if needed."""
129
- if self.sock is not None and not self.sock.closed:
130
- self.sock.close()
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._eagain(channel.request_shell)
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._eagain(channel.open_session)
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 execute(self, cmd, use_pty=False, channel=None):
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
- channel = self.open_session() if not channel else channel
239
- if use_pty:
240
- self._eagain(channel.request_pty, timeout=self.timeout)
241
- logger.debug("Executing command '%s'", cmd)
242
- self._eagain(channel.request_exec, cmd)
243
- return channel
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._eagain(channel.send_eof, timeout=self.timeout)
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._eagain(channel.close)
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 _eagain(self, func, *args, **kwargs):
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 _eagain_write(self, write_func, data):
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
- # This file is part of parallel-ssh.
1
+ # This file is part of parallel-ssh.
2
+ # Copyright (C) 2014-2025 Panos Kittenis.
3
+ # Copyright (C) 2014-2025 parallel-ssh Contributors.
2
4
  #
3
- # Copyright (C) 2014-2022 Panos Kittenis and contributors.
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License as published by the Free Software Foundation, version 2.1.
4
8
  #
5
- # This library is free software; you can redistribute it and/or
6
- # modify it under the terms of the GNU Lesser General Public
7
- # License as published by the Free Software Foundation, version 2.1.
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
8
13
  #
9
- # This library is distributed in the hope that it will be useful,
10
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
- # Lesser General Public License for more details.
13
- #
14
- # You should have received a copy of the GNU Lesser General Public
15
- # License along with this library; if not, write to the Free Software
16
- # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
 
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)