Contents

Advanced Usage

There are several more advanced use cases of ParallelSSH, such as tunneling (aka proxying) via an intermediate SSH server and per-host configuration and command substitution among others.

SSH Agent forwarding

SSH agent forwarding, what ssh -A does on the command line, is supported and enabled by default. Creating an object as ParallelSSHClient(hosts, forward_ssh_agent=False) will disable this behaviour.

Programmatic Private Keys

By default, ParallelSSH will use all keys in an available SSH agent and all available keys under the user's SSH directory (~/.ssh).

A private key can also be provided programmatically.

from pssh.utils import load_private_key
from pssh import ParallelSSHClient

client = ParallelSSHClient(hosts, pkey=load_private_key('my_key'))

Where my_key is a private key file in current working directory.

The helper function :py:func:`pssh.utils.load_private_key` will attempt to load all available key types and raises :mod:`pssh.exceptions.SSHException` if it cannot load the key file.

System Message: ERROR/3 (advanced.rst, line 32); backlink

Unknown interpreted text role "py:func".

System Message: ERROR/3 (advanced.rst, line 32); backlink

Unknown interpreted text role "mod".

Using an available SSH agent can also be disabled programmatically.

client = ParallelSSHClient(hosts, pkey=load_private_key('my_key'),
                           allow_agent=False)

Programmatic SSH Agent

It is also possible to programmatically provide an SSH agent for the client to use, instead of a system provided one. This is useful in cases where different hosts in the host list need different private keys and a system SSH agent is not available.

from pssh.agent import SSHAgent
from pssh.utils import load_private_key
from pssh import ParallelSSHClient

agent = SSHAgent()
agent.add_key(load_private_key('my_private_key_filename'))
agent.add_key(load_private_key('my_other_private_key_filename'))
hosts = ['my_host', 'my_other_host']

client = ParallelSSHClient(hosts, agent=agent)
client.run_command(<..>)

Supplying an agent object programmatically implies that a system SSH agent will not be used if available.

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, commonly used for additional security as only the bastion host needs to have access to the target host.

ParallelSSHClient ------> SSH Proxy server --------> SSH target host

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

hosts = [<..>]
client = ParallelSSHClient(hosts, proxy_host=bastion)

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

from pssh.utils import load_private_key

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

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

Per-Host Configuration

Sometimes, different hosts require different configuration like user names and passwords, ports and private keys. Capability is provided to supply per host configuration for such cases.

from pssh.utils import load_private_key

host_config = {'host1' : {'user': 'user1', 'password': 'pass',
                          'port': 2222,
                          'private_key': load_private_key(
                              'my_key.pem')},
               'host2' : {'user': 'user2', 'password': 'pass',
                          'port': 2223,
                          'private_key': load_private_key(
                              open('my_other_key.pem'))},
              }
hosts = host_config.keys()

client = ParallelSSHClient(hosts, host_config=host_config)
client.run_command('uname')
<..>

In the above example, host1 will use user name user1 and private key from my_key.pem and host2 will use user name user2 and private key from my_other_key.pem.

Per-Host Command substitution

For cases where different commands should be run each host, or the same command with different arguments, functionality exists to provide per-host command arguments for substitution.

The host_args keyword parameter to :py:func:`pssh.pssh_client.ParallelSSHClient.run_command` can be used to provide arguments to use to format the command string.

System Message: ERROR/3 (advanced.rst, line 120); backlink

Unknown interpreted text role "py:func".

Number of host_args items should be at least as many as number of hosts.

Any Python string format specification characters may be used in command string.

In the following example, first host in hosts list will use cmd host1_cmd second host host2_cmd and so on

output = client.run_command('%s', host_args=('host1_cmd',
                                             'host2_cmd',
                                             'host3_cmd',))

Command can also have multiple arguments to be substituted.

output = client.run_command('%s %s',
host_args=(('host1_cmd1', 'host1_cmd2'),
           ('host2_cmd1', 'host2_cmd2'),
           ('host3_cmd1', 'host3_cmd2'),))

A list of dictionaries can also be used as host_args for named argument substitution.

In the following example, first host in host list will use cmd host-index-0, second host host-index-1 and so on.

host_args=[{'cmd': 'host-index-%s' % (i,))
           for i in range(len(client.hosts))]
output = client.run_command('%(cmd)s', host_args=host_args)

Host list filtering and dynamic host list

Any type of iterator may be used as host list, including generator and list comprehension expressions.

List comprehension:
 
hosts = ['dc1.myhost1', 'dc2.myhost2']
client = ParallelSSHClient([h for h in hosts if h.find('dc1')])
Generator:
hosts = ['dc1.myhost1', 'dc2.myhost2']
client = ParallelSSHClient((h for h in hosts if h.find('dc1')))
Filter:
hosts = ['dc1.myhost1', 'dc2.myhost2']
client = ParallelSSHClient(filter(lambda h: h.find('dc1'), hosts))
client.run_command(<..>)

Note

Since generators by design only iterate over a sequence once then stop, client.hosts should be re-assigned after each call to run_command when using generators as target of client.hosts.

Overriding host list

Host list can be modified in place. Call to run_command will create new connections as necessary and output will only contain output for the hosts run_command executed on.

client.hosts = ['otherhost']
print(client.run_command('exit 0'))
{'otherhost': exit_code=None, <..>}