Skip to content

Commit 084696d

Browse files
authored
Tunnel server (#239)
* Re-write of tunneling implementation for performance and ease of use * SSHClient now creates and manages its own proxy connections via new tunnel * Added `connect_auth` parallel client function * Updated changelog * Updated documentation
1 parent 3ffb1d0 commit 084696d

17 files changed

+525
-529
lines changed

Changelog.rst

+12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
Change Log
22
============
33

4+
2.2.0
5+
+++++
6+
7+
Changes
8+
-------
9+
10+
* New single host tunneling, SSH proxy, implementation for increased performance.
11+
* Native ``SSHClient`` now accepts ``proxy_host``, ``proxy_port`` and associated parameters - see `API documentation <https://parallel-ssh.readthedocs.io/en/latest/config.html>`_.
12+
* Proxy configuration can now be provided via ``HostConfig``.
13+
* Added ``ParallelSSHClient.connect_auth`` function for connecting and authenticating to hosts in parallel.
14+
15+
416
2.1.0
517
+++++
618

README.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ Native code based client with extremely high performance - based on ``libssh2``
1414
.. image:: https://img.shields.io/pypi/v/parallel-ssh.svg
1515
:target: https://pypi.python.org/pypi/parallel-ssh
1616
:alt: Latest Version
17-
.. image:: https://travis-ci.org/ParallelSSH/parallel-ssh.svg?branch=master
18-
:target: https://travis-ci.org/ParallelSSH/parallel-ssh
17+
.. image:: https://circleci.com/gh/ParallelSSH/parallel-ssh/tree/master.svg?style=svg
18+
:target: https://circleci.com/gh/ParallelSSH/parallel-ssh
1919
.. image:: https://ci.appveyor.com/api/projects/status/github/parallelssh/parallel-ssh?svg=true&branch=master
2020
:target: https://ci.appveyor.com/project/pkittenis/parallel-ssh-4nme1
2121
.. image:: https://codecov.io/gh/ParallelSSH/parallel-ssh/branch/master/graph/badge.svg

doc/advanced.rst

+42-13
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,14 @@ Both private key and corresponding signed public certificate file must be provid
118118
``ssh-python`` :py:mod:`ParallelSSHClient <pssh.clients.ssh>` clients only.
119119

120120

121-
Tunnelling
122-
**********
121+
Proxy Hosts and Tunneling
122+
**************************
123123

124-
This is used in cases where the client does not have direct access to the target host and has to authenticate via an intermediary, also called a bastion host.
124+
This is used in cases where the client does not have direct access to the target host(s) and has to authenticate via an intermediary proxy, also called a bastion host.
125125

126-
Commonly used for additional security as only the proxy or bastion host needs to have access to the target host.
126+
Commonly used for additional security as only the proxy host needs to have access to the target host.
127127

128-
ParallelSSHClient ------> Proxy host --------> Target host
128+
Client ------> Proxy host --------> Target host
129129

130130
Proxy host can be configured as follows in the simplest case:
131131

@@ -134,27 +134,56 @@ Proxy host can be configured as follows in the simplest case:
134134
hosts = [<..>]
135135
client = ParallelSSHClient(hosts, proxy_host='bastion')
136136
137+
For single host clients:
138+
139+
.. code-block:: python
140+
141+
host = '<..>'
142+
client = SSHClient(host, proxy_host='proxy')
143+
137144
Configuration for the proxy host's user name, port, password and private key can also be provided, separate from target host configuration.
138145

139146
.. code-block:: python
140-
147+
141148
hosts = [<..>]
142-
client = ParallelSSHClient(hosts, user='target_host_user',
143-
proxy_host='bastion', proxy_user='my_proxy_user',
144-
proxy_port=2222,
145-
proxy_pkey='proxy.key')
149+
client = ParallelSSHClient(
150+
hosts, user='target_host_user',
151+
proxy_host='bastion',
152+
proxy_user='my_proxy_user',
153+
proxy_port=2222,
154+
proxy_pkey='proxy.key')
146155
147156
Where ``proxy.key`` is a filename containing private key to use for proxy host authentication.
148157

149158
In the above example, connections to the target hosts are made via SSH through ``my_proxy_user@bastion:2222`` -> ``target_host_user@<host>``.
150159

160+
161+
Per Host Proxy Configuration
162+
=============================
163+
164+
Proxy host can be configured in Per-Host Configuration:
165+
166+
.. code-block:: python
167+
168+
hosts = [<..>]
169+
host_config = [
170+
HostConfig(proxy_host='127.0.0.1'),
171+
HostConfig(proxy_host='127.0.0.2'),
172+
HostConfig(proxy_host='127.0.0.3'),
173+
HostConfig(proxy_host='127.0.0.4'),
174+
]
175+
clieent = ParallelSSHClient(hosts, host_config=host_config)
176+
output = client.run_command('echo me')
177+
178+
See :py:mod:`HostConfig <pssh.config.HostConfig>` for all possible configuration.
179+
151180
.. note::
152181

153-
The current implementation of tunnelling suffers from poor performance when first establishing connections to many hosts - this is due to be resolved in a future release.
182+
New tunneling implementation from `2.2.0` for highest performance.
154183

155-
Proxy host connections are asynchronous and use the SSH protocol's native TCP tunnelling - aka local port forward. No external commands or processes are used for the proxy connection, unlike the `ProxyCommand` directive in OpenSSH and other utilities.
184+
Connecting to dozens or more hosts via a single proxy host will impact performance considerably.
156185

157-
While connections initiated by ``parallel-ssh`` are asynchronous, connections from proxy host -> target hosts may not be, depending on SSH server implementation. If only one proxy host is used to connect to a large number of target hosts and proxy SSH server connections are *not* asynchronous, this may adversely impact performance on the proxy host.
186+
See above for using host specific proxy configuration.
158187

159188
Join and Output Timeouts
160189
**************************

doc/tunnel.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
Native Tunnel
22
==============
33

4-
Note this module is only intended for use as a proxy host for :py:class:`ParallelSSHClient <pssh.clients.native.parallel.ParallelSSHClient>`. It will very likely need sub-classing and further enhancing to be used for other purposes.
4+
This module provides general purpose functionality for tunneling connections via an intermediary SSH proxy.
5+
6+
Clients connect to a provided local port and get traffic forwarded to/from the target host via an SSH proxy.
57

68
.. automodule:: pssh.clients.native.tunnel
79
:members:

examples/parallel_commands.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
for cmd in cmds:
1313
output.append(client.run_command(cmd, stop_on_errors=False, return_list=True))
1414
end = datetime.datetime.now()
15-
print("Started %s commands on %s host(s) in %s" % (
15+
print("Started %s 'sleep 5' commands on %s host(s) in %s" % (
1616
len(cmds), len(hosts), end-start,))
1717
start = datetime.datetime.now()
1818
for _output in output:

examples/pssh_local.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,31 @@
2020
and ParallelSSHClient.
2121
"""
2222

23-
from pssh import SSHClient, ParallelSSHClient, utils
2423
import logging
24+
2525
from pprint import pprint
2626

27-
utils.enable_host_logger()
28-
utils.enable_logger(utils.logger)
27+
from pssh.clients import SSHClient, ParallelSSHClient
28+
2929

3030
def test():
3131
"""Perform ls and copy file with SSHClient on localhost"""
3232
client = SSHClient('localhost')
33-
channel, host, stdout, stderr = client.exec_command('ls -ltrh')
34-
for line in stdout:
35-
pprint(line.strip())
33+
output = client.run_command('ls -ltrh')
34+
for line in output.stdout:
35+
print(line)
3636
client.copy_file('../test', 'test_dir/test')
3737

38+
3839
def test_parallel():
3940
"""Perform ls and copy file with ParallelSSHClient on localhost.
40-
41+
4142
Two identical hosts cause the same command to be executed
4243
twice on the same host in two parallel connections.
4344
In printed output there will be two identical lines per printed per
4445
line of `ls -ltrh` output as output is printed by host_logger as it
4546
becomes available and commands are executed in parallel
46-
47+
4748
Host output key is de-duplicated so that output for the two
4849
commands run on the same host(s) is not lost
4950
"""
@@ -52,7 +53,8 @@ def test_parallel():
5253
client.join(output)
5354
pprint(output)
5455
cmds = client.copy_file('../test', 'test_dir/test')
55-
client.pool.join()
56+
joinall(cmds, raise_error=True)
57+
5658

5759
if __name__ == "__main__":
5860
test()

pssh/clients/base/parallel.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,26 @@ def reset_output_generators(self, host_out, timeout=None,
190190

191191
def _get_host_config_values(self, host_i, host):
192192
if self.host_config is None:
193-
return self.user, self.port, self.password, self.pkey
193+
return self.user, self.port, self.password, self.pkey, \
194+
getattr(self, 'proxy_host', None), \
195+
getattr(self, 'proxy_port', None), getattr(self, 'proxy_user', None), \
196+
getattr(self, 'proxy_password', None), getattr(self, 'proxy_pkey', None)
194197
elif isinstance(self.host_config, list):
195-
_user = self.host_config[host_i].user
196-
_port = self.host_config[host_i].port
197-
_password = self.host_config[host_i].password
198-
_pkey = self.host_config[host_i].private_key
199-
return _user, _port, _password, _pkey
198+
config = self.host_config[host_i]
199+
return config.user or self.user, config.port or self.port, \
200+
config.password or self.password, config.private_key or self.pkey, \
201+
config.proxy_host or getattr(self, 'proxy_host', None), \
202+
config.proxy_port or getattr(self, 'proxy_port', None), \
203+
config.proxy_user or getattr(self, 'proxy_user', None), \
204+
config.proxy_password or getattr(self, 'proxy_password', None), \
205+
config.proxy_pkey or getattr(self, 'proxy_pkey', None)
200206
elif isinstance(self.host_config, dict):
201207
_user = self.host_config.get(host, {}).get('user', self.user)
202208
_port = self.host_config.get(host, {}).get('port', self.port)
203209
_password = self.host_config.get(host, {}).get(
204210
'password', self.password)
205211
_pkey = self.host_config.get(host, {}).get('private_key', self.pkey)
206-
return _user, _port, _password, _pkey
212+
return _user, _port, _password, _pkey, None, None, None, None, None
207213

208214
def _run_command(self, host_i, host, command, sudo=False, user=None,
209215
shell=None, use_pty=False,
@@ -221,6 +227,24 @@ def _run_command(self, host_i, host, command, sudo=False, user=None,
221227
logger.error("Failed to run on host %s - %s", host, ex)
222228
raise ex
223229

230+
def connect_auth(self):
231+
"""Connect to and authenticate with all hosts in parallel.
232+
233+
This function can be used to perform connection and authentication outside of
234+
command functions like ``run_command`` or ``copy_file`` so the two operations,
235+
login and running a remote command, can be separated.
236+
237+
It is not required to be called prior to any other functions.
238+
239+
Connections and authentication is performed in parallel by this and all other
240+
functions.
241+
242+
:returns: list of greenlets to ``joinall`` with.
243+
:rtype: list(:py:mod:`gevent.greenlet.Greenlet`)
244+
"""
245+
cmds = [spawn(self._make_ssh_client, i, host) for i, host in enumerate(self.hosts)]
246+
return cmds
247+
224248
def _consume_output(self, stdout, stderr):
225249
for line in stdout:
226250
pass

pssh/clients/base/single.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(self, host,
5858
retry_delay=RETRY_DELAY,
5959
allow_agent=True, timeout=None,
6060
proxy_host=None,
61+
proxy_port=None,
6162
_auth_thread_pool=True,
6263
identity_auth=True):
6364
self._auth_thread_pool = _auth_thread_pool
@@ -76,13 +77,14 @@ def __init__(self, host,
7677
self.allow_agent = allow_agent
7778
self.session = None
7879
self._host = proxy_host if proxy_host else host
80+
self._port = proxy_port if proxy_port else self.port
7981
self.pkey = _validate_pkey_path(pkey, self.host)
8082
self.identity_auth = identity_auth
8183
self._keepalive_greenlet = None
8284
self._init()
8385

8486
def _init(self):
85-
self._connect(self._host, self.port)
87+
self._connect(self._host, self._port)
8688
self._init_session()
8789
self._auth_retry()
8890
self._keepalive()
@@ -127,7 +129,7 @@ def _connect_init_session_retry(self, retries):
127129
except Exception:
128130
pass
129131
sleep(self.retry_delay)
130-
self._connect(self._host, self.port, retries=retries)
132+
self._connect(self._host, self._port, retries=retries)
131133
return self._init_session(retries=retries)
132134

133135
def _connect(self, host, port, retries=1):

0 commit comments

Comments
 (0)