Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Change Log
============

2.2.0
+++++

Changes
-------

* New single host tunneling, SSH proxy, implementation for increased performance.
* Native ``SSHClient`` now accepts ``proxy_host``, ``proxy_port`` and associated parameters - see `API documentation <https://parallel-ssh.readthedocs.io/en/latest/config.html>`_.
* Proxy configuration can now be provided via ``HostConfig``.
* Added ``ParallelSSHClient.connect_auth`` function for connecting and authenticating to hosts in parallel.


2.1.0
+++++

Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Native code based client with extremely high performance - based on ``libssh2``
.. image:: https://img.shields.io/pypi/v/parallel-ssh.svg
:target: https://pypi.python.org/pypi/parallel-ssh
:alt: Latest Version
.. image:: https://travis-ci.org/ParallelSSH/parallel-ssh.svg?branch=master
:target: https://travis-ci.org/ParallelSSH/parallel-ssh
.. image:: https://circleci.com/gh/ParallelSSH/parallel-ssh/tree/master.svg?style=svg
:target: https://circleci.com/gh/ParallelSSH/parallel-ssh
.. image:: https://ci.appveyor.com/api/projects/status/github/parallelssh/parallel-ssh?svg=true&branch=master
:target: https://ci.appveyor.com/project/pkittenis/parallel-ssh-4nme1
.. image:: https://codecov.io/gh/ParallelSSH/parallel-ssh/branch/master/graph/badge.svg
Expand Down
55 changes: 42 additions & 13 deletions doc/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ Both private key and corresponding signed public certificate file must be provid
``ssh-python`` :py:mod:`ParallelSSHClient <pssh.clients.ssh>` clients only.


Tunnelling
**********
Proxy Hosts and Tunneling
**************************

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.
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.

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

ParallelSSHClient ------> Proxy host --------> Target host
Client ------> Proxy host --------> Target host

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

Expand All @@ -134,27 +134,56 @@ Proxy host can be configured as follows in the simplest case:
hosts = [<..>]
client = ParallelSSHClient(hosts, proxy_host='bastion')

For single host clients:

.. code-block:: python

host = '<..>'
client = SSHClient(host, proxy_host='proxy')

Configuration for the proxy host's user name, port, password and private key can also be provided, separate from target host configuration.

.. code-block:: python

hosts = [<..>]
client = ParallelSSHClient(hosts, user='target_host_user',
proxy_host='bastion', proxy_user='my_proxy_user',
proxy_port=2222,
proxy_pkey='proxy.key')
client = ParallelSSHClient(
hosts, user='target_host_user',
proxy_host='bastion',
proxy_user='my_proxy_user',
proxy_port=2222,
proxy_pkey='proxy.key')

Where ``proxy.key`` is a filename containing private key to use for proxy host authentication.

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


Per Host Proxy Configuration
=============================

Proxy host can be configured in Per-Host Configuration:

.. code-block:: python

hosts = [<..>]
host_config = [
HostConfig(proxy_host='127.0.0.1'),
HostConfig(proxy_host='127.0.0.2'),
HostConfig(proxy_host='127.0.0.3'),
HostConfig(proxy_host='127.0.0.4'),
]
clieent = ParallelSSHClient(hosts, host_config=host_config)
output = client.run_command('echo me')

See :py:mod:`HostConfig <pssh.config.HostConfig>` for all possible configuration.

.. note::

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.
New tunneling implementation from `2.2.0` for highest performance.

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.
Connecting to dozens or more hosts via a single proxy host will impact performance considerably.

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.
See above for using host specific proxy configuration.

Join and Output Timeouts
**************************
Expand Down
4 changes: 3 additions & 1 deletion doc/tunnel.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
Native Tunnel
==============

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.
This module provides general purpose functionality for tunneling connections via an intermediary SSH proxy.

Clients connect to a provided local port and get traffic forwarded to/from the target host via an SSH proxy.

.. automodule:: pssh.clients.native.tunnel
:members:
Expand Down
2 changes: 1 addition & 1 deletion examples/parallel_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
for cmd in cmds:
output.append(client.run_command(cmd, stop_on_errors=False, return_list=True))
end = datetime.datetime.now()
print("Started %s commands on %s host(s) in %s" % (
print("Started %s 'sleep 5' commands on %s host(s) in %s" % (
len(cmds), len(hosts), end-start,))
start = datetime.datetime.now()
for _output in output:
Expand Down
20 changes: 11 additions & 9 deletions examples/pssh_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,31 @@
and ParallelSSHClient.
"""

from pssh import SSHClient, ParallelSSHClient, utils
import logging

from pprint import pprint

utils.enable_host_logger()
utils.enable_logger(utils.logger)
from pssh.clients import SSHClient, ParallelSSHClient


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


def test_parallel():
"""Perform ls and copy file with ParallelSSHClient on localhost.

Two identical hosts cause the same command to be executed
twice on the same host in two parallel connections.
In printed output there will be two identical lines per printed per
line of `ls -ltrh` output as output is printed by host_logger as it
becomes available and commands are executed in parallel

Host output key is de-duplicated so that output for the two
commands run on the same host(s) is not lost
"""
Expand All @@ -52,7 +53,8 @@ def test_parallel():
client.join(output)
pprint(output)
cmds = client.copy_file('../test', 'test_dir/test')
client.pool.join()
joinall(cmds, raise_error=True)


if __name__ == "__main__":
test()
Expand Down
38 changes: 31 additions & 7 deletions pssh/clients/base/parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,20 +190,26 @@ def reset_output_generators(self, host_out, timeout=None,

def _get_host_config_values(self, host_i, host):
if self.host_config is None:
return self.user, self.port, self.password, self.pkey
return self.user, self.port, self.password, self.pkey, \
getattr(self, 'proxy_host', None), \
getattr(self, 'proxy_port', None), getattr(self, 'proxy_user', None), \
getattr(self, 'proxy_password', None), getattr(self, 'proxy_pkey', None)
elif isinstance(self.host_config, list):
_user = self.host_config[host_i].user
_port = self.host_config[host_i].port
_password = self.host_config[host_i].password
_pkey = self.host_config[host_i].private_key
return _user, _port, _password, _pkey
config = self.host_config[host_i]
return config.user or self.user, config.port or self.port, \
config.password or self.password, config.private_key or self.pkey, \
config.proxy_host or getattr(self, 'proxy_host', None), \
config.proxy_port or getattr(self, 'proxy_port', None), \
config.proxy_user or getattr(self, 'proxy_user', None), \
config.proxy_password or getattr(self, 'proxy_password', None), \
config.proxy_pkey or getattr(self, 'proxy_pkey', None)
elif isinstance(self.host_config, dict):
_user = self.host_config.get(host, {}).get('user', self.user)
_port = self.host_config.get(host, {}).get('port', self.port)
_password = self.host_config.get(host, {}).get(
'password', self.password)
_pkey = self.host_config.get(host, {}).get('private_key', self.pkey)
return _user, _port, _password, _pkey
return _user, _port, _password, _pkey, None, None, None, None, None

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

def connect_auth(self):
"""Connect to and authenticate with all hosts in parallel.

This function can be used to perform connection and authentication outside of
command functions like ``run_command`` or ``copy_file`` so the two operations,
login and running a remote command, can be separated.

It is not required to be called prior to any other functions.

Connections and authentication is performed in parallel by this and all other
functions.

:returns: list of greenlets to ``joinall`` with.
:rtype: list(:py:mod:`gevent.greenlet.Greenlet`)
"""
cmds = [spawn(self._make_ssh_client, i, host) for i, host in enumerate(self.hosts)]
return cmds

def _consume_output(self, stdout, stderr):
for line in stdout:
pass
Expand Down
6 changes: 4 additions & 2 deletions pssh/clients/base/single.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(self, host,
retry_delay=RETRY_DELAY,
allow_agent=True, timeout=None,
proxy_host=None,
proxy_port=None,
_auth_thread_pool=True,
identity_auth=True):
self._auth_thread_pool = _auth_thread_pool
Expand All @@ -76,13 +77,14 @@ def __init__(self, host,
self.allow_agent = allow_agent
self.session = None
self._host = proxy_host if proxy_host else host
self._port = proxy_port if proxy_port else self.port
self.pkey = _validate_pkey_path(pkey, self.host)
self.identity_auth = identity_auth
self._keepalive_greenlet = None
self._init()

def _init(self):
self._connect(self._host, self.port)
self._connect(self._host, self._port)
self._init_session()
self._auth_retry()
self._keepalive()
Expand Down Expand Up @@ -127,7 +129,7 @@ def _connect_init_session_retry(self, retries):
except Exception:
pass
sleep(self.retry_delay)
self._connect(self._host, self.port, retries=retries)
self._connect(self._host, self._port, retries=retries)
return self._init_session(retries=retries)

def _connect(self, host, port, retries=1):
Expand Down
Loading