Skip to content

Commit 29b7841

Browse files
authored
Compression support (#405)
* Added compression support to all clients and HostConfig entries - Resolves #252 * Added CI tests for compression support. * Added tests for new host config entries * Updated CI cfg
1 parent 987dd58 commit 29b7841

File tree

13 files changed

+83
-6
lines changed

13 files changed

+83
-6
lines changed

.circleci/config.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ jobs:
3939
command: |
4040
pytest ci/integration_tests
4141
name: Integration tests
42+
max_auto_reruns: 3
43+
auto_rerun_delay: 10s
4244
- run:
4345
command: |
4446
cd doc; make html
@@ -86,9 +88,9 @@ workflows:
8688
parameters:
8789
python_ver:
8890
- "3.8"
89-
- "3.10"
9091
- "3.11"
9192
- "3.12"
93+
- "3.13"
9294
filters:
9395
tags:
9496
ignore: /.*/

Changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Change Log
22
==========
33

4+
2.15.0
5+
++++++
6+
7+
Changes
8+
-------
9+
10+
* Added compression support for all clients via `SSHClient(compress=True)`, `ParallelSSHClient(compress=True)` and
11+
`HostConfig(compress=True)` - defaults to off. #252
12+
* Updated minimum `ssh2-python` and `ssh-python` requirements.
13+
14+
415
2.14.0
516
++++++
617

ci/integration_tests/libssh2_clients/test_parallel_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,5 +1934,17 @@ def test_no_ipv6(self):
19341934
self.assertEqual(self.host, host_out.host)
19351935
self.assertIsInstance(host_out.exception, NoIPv6AddressFoundError)
19361936

1937+
def test_compression_enabled(self):
1938+
client = ParallelSSHClient([self.host], port=self.port, pkey=self.user_key, num_retries=1, compress=True)
1939+
output = client.run_command(self.cmd, stop_on_errors=False)
1940+
client.join(output)
1941+
self.assertTrue(client._host_clients[0, self.host].compress)
1942+
expected_exit_code = 0
1943+
expected_stdout = [self.resp]
1944+
stdout = list(output[0].stdout)
1945+
exit_code = output[0].exit_code
1946+
self.assertEqual(expected_exit_code, exit_code)
1947+
self.assertEqual(expected_stdout, stdout)
1948+
19371949
# TODO:
19381950
# * password auth

ci/integration_tests/libssh_clients/test_parallel_client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,18 @@ def test_ipv6(self, gsocket):
515515
self.assertEqual(hosts[0], host_out.host)
516516
self.assertIsInstance(host_out.exception, TypeError)
517517

518+
def test_compression_enabled(self):
519+
client = ParallelSSHClient([self.host], port=self.port, pkey=self.user_key, num_retries=1, compress=True)
520+
output = client.run_command(self.cmd, stop_on_errors=False)
521+
client.join(output)
522+
self.assertTrue(client._host_clients[0, self.host].compress)
523+
expected_exit_code = 0
524+
expected_stdout = [self.resp]
525+
stdout = list(output[0].stdout)
526+
exit_code = output[0].exit_code
527+
self.assertEqual(expected_exit_code, exit_code)
528+
self.assertEqual(expected_stdout, stdout)
529+
518530
# def test_multiple_run_command_timeout(self):
519531
# client = ParallelSSHClient([self.host], port=self.port,
520532
# pkey=self.user_key)

pssh/clients/base/parallel.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
5555
gssapi_client_identity=None,
5656
gssapi_delegate_credentials=False,
5757
forward_ssh_agent=False,
58+
compress=False,
5859
_auth_thread_pool=True,
5960
):
6061
self.allow_agent = allow_agent
@@ -86,6 +87,7 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
8687
self.gssapi_server_identity = gssapi_server_identity
8788
self.gssapi_client_identity = gssapi_client_identity
8889
self.gssapi_delegate_credentials = gssapi_delegate_credentials
90+
self.compress = compress
8991
self._auth_thread_pool = _auth_thread_pool
9092
self._check_host_config()
9193

pssh/clients/base/single.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ def __init__(self, host,
226226
_auth_thread_pool=True,
227227
identity_auth=True,
228228
ipv6_only=False,
229+
compress=False,
229230
):
230231
super(PollMixIn, self).__init__()
231232
self._auth_thread_pool = _auth_thread_pool
@@ -245,6 +246,7 @@ def __init__(self, host,
245246
self.identity_auth = identity_auth
246247
self._keepalive_greenlet = None
247248
self.ipv6_only = ipv6_only
249+
self.compress = compress
248250
self._pool = Pool()
249251
self._init()
250252

pssh/clients/native/parallel.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
3737
forward_ssh_agent=False,
3838
keepalive_seconds=60, identity_auth=True,
3939
ipv6_only=False,
40+
compress=False,
4041
):
4142
"""
4243
:param hosts: Hosts to connect to
@@ -115,6 +116,8 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
115116
for the host(s) or raise NoIPv6AddressFoundError otherwise. Note this will
116117
disable connecting to an IPv4 address if an IP address is provided instead.
117118
:type ipv6_only: bool
119+
:param compress: Enable/Disable compression on the client. Defaults to off.
120+
:type compress: bool
118121
119122
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
120123
provided private key.
@@ -126,6 +129,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
126129
host_config=host_config, retry_delay=retry_delay,
127130
identity_auth=identity_auth,
128131
ipv6_only=ipv6_only,
132+
compress=compress,
129133
)
130134
self.proxy_host = proxy_host
131135
self.proxy_port = proxy_port
@@ -232,6 +236,7 @@ def _make_ssh_client(self, host, cfg, _pkey_data):
232236
keepalive_seconds=cfg.keepalive_seconds or self.keepalive_seconds,
233237
identity_auth=cfg.identity_auth or self.identity_auth,
234238
ipv6_only=cfg.ipv6_only or self.ipv6_only,
239+
compress=cfg.compress or self.compress,
235240
)
236241
return _client
237242

pssh/clients/native/single.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN
2828
from ssh2.exceptions import SFTPHandleError, SFTPProtocolError, \
2929
Timeout as SSH2Timeout
30-
from ssh2.session import Session, LIBSSH2_SESSION_BLOCK_INBOUND, LIBSSH2_SESSION_BLOCK_OUTBOUND
30+
from ssh2.session import Session, LIBSSH2_SESSION_BLOCK_INBOUND, LIBSSH2_SESSION_BLOCK_OUTBOUND, LIBSSH2_FLAG_COMPRESS
3131
from ssh2.sftp import LIBSSH2_FXF_READ, LIBSSH2_FXF_CREAT, LIBSSH2_FXF_WRITE, \
3232
LIBSSH2_FXF_TRUNC, LIBSSH2_SFTP_S_IRUSR, LIBSSH2_SFTP_S_IRGRP, \
3333
LIBSSH2_SFTP_S_IWUSR, LIBSSH2_SFTP_S_IXUSR, LIBSSH2_SFTP_S_IROTH, \
@@ -110,6 +110,7 @@ def __init__(self, host,
110110
keepalive_seconds=60,
111111
identity_auth=True,
112112
ipv6_only=False,
113+
compress=False,
113114
):
114115
"""
115116
:param host: Host name or IP to connect to.
@@ -158,6 +159,8 @@ def __init__(self, host,
158159
for the host or raise NoIPv6AddressFoundError otherwise. Note this will
159160
disable connecting to an IPv4 address if an IP address is provided instead.
160161
:type ipv6_only: bool
162+
:param compress: Enable/Disable compression on the client. Defaults to off.
163+
:type compress: bool
161164
162165
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
163166
provided private key.
@@ -182,6 +185,7 @@ def __init__(self, host,
182185
timeout=timeout,
183186
keepalive_seconds=keepalive_seconds,
184187
identity_auth=identity_auth,
188+
compress=compress,
185189
)
186190
proxy_host = '127.0.0.1'
187191
self._chan_stdout_lock = RLock()
@@ -194,6 +198,7 @@ def __init__(self, host,
194198
proxy_host=proxy_host, proxy_port=proxy_port,
195199
identity_auth=identity_auth,
196200
ipv6_only=ipv6_only,
201+
compress=compress,
197202
)
198203

199204
def _shell(self, channel):
@@ -206,7 +211,9 @@ def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
206211
allow_agent=True, timeout=None,
207212
forward_ssh_agent=False,
208213
keepalive_seconds=60,
209-
identity_auth=True):
214+
identity_auth=True,
215+
compress=False,
216+
):
210217
assert isinstance(self.port, int)
211218
try:
212219
self._proxy_client = SSHClient(
@@ -216,6 +223,7 @@ def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
216223
timeout=timeout, forward_ssh_agent=forward_ssh_agent,
217224
identity_auth=identity_auth,
218225
keepalive_seconds=keepalive_seconds,
226+
compress=compress,
219227
_auth_thread_pool=False)
220228
except Exception as ex:
221229
msg = "Proxy authentication failed. " \
@@ -263,6 +271,8 @@ def configure_keepalive(self):
263271

264272
def _init_session(self, retries=1):
265273
self.session = Session()
274+
if self.compress:
275+
self.session.flag(LIBSSH2_FLAG_COMPRESS)
266276

267277
if self.timeout:
268278
# libssh2 timeout is in ms

pssh/clients/ssh/parallel.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
4040
gssapi_delegate_credentials=False,
4141
identity_auth=True,
4242
ipv6_only=False,
43+
compress=False,
4344
):
4445
"""
4546
:param hosts: Hosts to connect to
@@ -114,6 +115,8 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
114115
for the host or raise NoIPv6AddressFoundError otherwise. Note this will
115116
disable connecting to an IPv4 address if an IP address is provided instead.
116117
:type ipv6_only: bool
118+
:param compress: Enable/Disable compression on the client. Defaults to off.
119+
:type compress: bool
117120
118121
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
119122
provided private key.
@@ -125,6 +128,7 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
125128
host_config=host_config, retry_delay=retry_delay,
126129
identity_auth=identity_auth,
127130
ipv6_only=ipv6_only,
131+
compress=compress,
128132
)
129133
self.pkey = _validate_pkey(pkey)
130134
self.cert_file = _validate_pkey_path(cert_file)
@@ -228,5 +232,6 @@ def _make_ssh_client(self, host, cfg, _pkey_data):
228232
gssapi_client_identity=self.gssapi_client_identity,
229233
gssapi_delegate_credentials=self.gssapi_delegate_credentials,
230234
cert_file=cfg.cert_file,
235+
compress=cfg.compress or self.compress,
231236
)
232237
return _client

pssh/clients/ssh/single.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(self, host,
5050
gssapi_client_identity=None,
5151
gssapi_delegate_credentials=False,
5252
ipv6_only=False,
53+
compress=False,
5354
_auth_thread_pool=True):
5455
""":param host: Host name or IP to connect to.
5556
:type host: str
@@ -107,6 +108,8 @@ def __init__(self, host,
107108
for the host or raise NoIPv6AddressFoundError otherwise. Note this will
108109
disable connecting to an IPv4 address if an IP address is provided instead.
109110
:type ipv6_only: bool
111+
:param compress: Enable/Disable compression on the client. Defaults to off.
112+
:type compress: bool
110113
111114
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
112115
provided private key.
@@ -124,6 +127,7 @@ def __init__(self, host,
124127
timeout=timeout,
125128
identity_auth=identity_auth,
126129
ipv6_only=ipv6_only,
130+
compress=compress,
127131
)
128132

129133
def _disconnect(self):
@@ -151,6 +155,8 @@ def _init_session(self, retries=1):
151155
self.session = Session()
152156
self.session.options_set(options.USER, self.user)
153157
self.session.options_set(options.HOST, self.host)
158+
if self.compress:
159+
self.session.options_set(options.COMPRESSION, "yes")
154160
self.session.options_set_port(self.port)
155161
if self.gssapi_server_identity:
156162
self.session.options_set(

0 commit comments

Comments
 (0)