Skip to content

Commit 1b44e9a

Browse files
authored
Add optional alias parameter to host config (#355)
* added optional alias parameter to single clients and HostConfig for configuration from parallel clients. This is useful for weird ssh proxies like cyberark PAM. Without this, it is difficult to identify the source of the output, as they all have the same host name. * Added tests. * Updated docstrings.
1 parent cd9836d commit 1b44e9a

File tree

11 files changed

+50
-17
lines changed

11 files changed

+50
-17
lines changed

pssh/clients/base/parallel.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ def _get_output_from_cmds(self, cmds, raise_error=False):
231231

232232
def _get_output_from_greenlet(self, cmd_i, cmd, raise_error=False):
233233
host = self.hosts[cmd_i]
234+
alias = self._get_host_config(cmd_i, host).alias
234235
try:
235236
host_out = cmd.get()
236237
return host_out
@@ -239,8 +240,7 @@ def _get_output_from_greenlet(self, cmd_i, cmd, raise_error=False):
239240
ex = Timeout()
240241
if raise_error:
241242
raise ex
242-
return HostOutput(host, None, None, None,
243-
exception=ex)
243+
return HostOutput(host, None, None, None, exception=ex, alias=alias)
244244

245245
def get_last_output(self, cmds=None):
246246
"""Get output for last commands executed by ``run_command``.
@@ -272,6 +272,7 @@ def _get_host_config(self, host_i, host):
272272
gssapi_server_identity=self.gssapi_server_identity,
273273
gssapi_client_identity=self.gssapi_client_identity,
274274
gssapi_delegate_credentials=self.gssapi_delegate_credentials,
275+
alias=None,
275276
)
276277
return config
277278
elif not isinstance(self.host_config, list):

pssh/clients/base/single.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ class BaseSSHClient(object):
159159

160160
def __init__(self, host,
161161
user=None, password=None, port=None,
162-
pkey=None,
162+
pkey=None, alias=None,
163163
num_retries=DEFAULT_RETRIES,
164164
retry_delay=RETRY_DELAY,
165165
allow_agent=True, timeout=None,
@@ -171,6 +171,7 @@ def __init__(self, host,
171171
):
172172
self._auth_thread_pool = _auth_thread_pool
173173
self.host = host
174+
self.alias = alias
174175
self.user = user if user else getuser()
175176
self.password = password
176177
self.port = port if port else 22
@@ -409,7 +410,7 @@ def _make_host_output(self, channel, encoding, read_timeout):
409410
stdout=BufferData(rw_buffer=_stdout_buffer, reader=_stdout_reader),
410411
stderr=BufferData(rw_buffer=_stderr_buffer, reader=_stderr_reader))
411412
host_out = HostOutput(
412-
host=self.host, channel=channel, stdin=Stdin(channel, self),
413+
host=self.host, alias=self.alias, channel=channel, stdin=Stdin(channel, self),
413414
client=self, encoding=encoding, read_timeout=read_timeout,
414415
buffers=_buffers,
415416
)

pssh/clients/native/parallel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ def _make_ssh_client(self, host, cfg, _pkey_data):
231231
_client = SSHClient(
232232
host, user=cfg.user or self.user, password=cfg.password or self.password, port=cfg.port or self.port,
233233
pkey=_pkey_data, num_retries=cfg.num_retries or self.num_retries,
234+
alias=cfg.alias,
234235
timeout=cfg.timeout or self.timeout,
235236
allow_agent=cfg.allow_agent or self.allow_agent, retry_delay=cfg.retry_delay or self.retry_delay,
236237
proxy_host=cfg.proxy_host or self.proxy_host,

pssh/clients/native/single.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class SSHClient(BaseSSHClient):
5050

5151
def __init__(self, host,
5252
user=None, password=None, port=None,
53-
pkey=None,
53+
pkey=None, alias=None,
5454
num_retries=DEFAULT_RETRIES,
5555
retry_delay=RETRY_DELAY,
5656
allow_agent=True, timeout=None,
@@ -70,6 +70,8 @@ def __init__(self, host,
7070
:type user: str
7171
:param password: Password to use for password authentication.
7272
:type password: str
73+
:param alias: Use an alias for this host.
74+
:type alias: str
7375
:param port: SSH port to connect to. Defaults to SSH default (22)
7476
:type port: int
7577
:param pkey: Private key file path to use for authentication. Path must
@@ -115,6 +117,7 @@ def __init__(self, host,
115117
self.keepalive_seconds = keepalive_seconds
116118
self._keepalive_greenlet = None
117119
self._proxy_client = None
120+
self.alias = alias
118121
self.host = host
119122
self.port = port if port is not None else 22
120123
if proxy_host is not None:
@@ -133,7 +136,7 @@ def __init__(self, host,
133136
proxy_host = '127.0.0.1'
134137
self._chan_lock = RLock()
135138
super(SSHClient, self).__init__(
136-
host, user=user, password=password, port=port, pkey=pkey,
139+
host, user=user, password=password, alias=alias, port=port, pkey=pkey,
137140
num_retries=num_retries, retry_delay=retry_delay,
138141
allow_agent=allow_agent, _auth_thread_pool=_auth_thread_pool,
139142
timeout=timeout,
@@ -146,7 +149,7 @@ def _shell(self, channel):
146149
return self._eagain(channel.shell)
147150

148151
def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
149-
user=None, password=None,
152+
user=None, password=None, alias=None,
150153
num_retries=DEFAULT_RETRIES,
151154
retry_delay=RETRY_DELAY,
152155
allow_agent=True, timeout=None,
@@ -156,7 +159,7 @@ def _connect_proxy(self, proxy_host, proxy_port, proxy_pkey,
156159
assert isinstance(self.port, int)
157160
try:
158161
self._proxy_client = SSHClient(
159-
proxy_host, port=proxy_port, pkey=proxy_pkey,
162+
proxy_host, port=proxy_port, pkey=proxy_pkey, alias=alias,
160163
num_retries=num_retries, user=user, password=password,
161164
retry_delay=retry_delay, allow_agent=allow_agent,
162165
timeout=timeout, forward_ssh_agent=forward_ssh_agent,

pssh/clients/ssh/parallel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ def _make_ssh_client(self, host, cfg, _pkey_data):
217217
_client = SSHClient(
218218
host, user=cfg.user or self.user, password=cfg.password or self.password, port=cfg.port or self.port,
219219
pkey=_pkey_data, num_retries=cfg.num_retries or self.num_retries,
220+
alias=cfg.alias,
220221
timeout=cfg.timeout or self.timeout,
221222
allow_agent=cfg.allow_agent or self.allow_agent, retry_delay=cfg.retry_delay or self.retry_delay,
222223
_auth_thread_pool=cfg.auth_thread_pool or self._auth_thread_pool,

pssh/clients/ssh/single.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class SSHClient(BaseSSHClient):
4040

4141
def __init__(self, host,
4242
user=None, password=None, port=None,
43-
pkey=None,
43+
pkey=None, alias=None,
4444
cert_file=None,
4545
num_retries=DEFAULT_RETRIES,
4646
retry_delay=RETRY_DELAY,
@@ -60,6 +60,8 @@ def __init__(self, host,
6060
:type password: str
6161
:param port: SSH port to connect to. Defaults to SSH default (22)
6262
:type port: int
63+
:param alias: Use an alias for this host.
64+
:type alias: str
6365
:param pkey: Private key file path to use for authentication. Path must
6466
be either absolute path or relative to user home directory
6567
like ``~/<path>``.
@@ -114,7 +116,7 @@ def __init__(self, host,
114116
self.gssapi_client_identity = gssapi_client_identity
115117
self.gssapi_delegate_credentials = gssapi_delegate_credentials
116118
super(SSHClient, self).__init__(
117-
host, user=user, password=password, port=port, pkey=pkey,
119+
host, user=user, password=password, port=port, pkey=pkey, alias=alias,
118120
num_retries=num_retries, retry_delay=retry_delay,
119121
allow_agent=allow_agent,
120122
_auth_thread_pool=_auth_thread_pool,

pssh/config.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ class HostConfig(object):
2525
Used to hold individual configuration for each host in ParallelSSHClient host list.
2626
"""
2727
__slots__ = ('user', 'port', 'password', 'private_key', 'allow_agent',
28-
'num_retries', 'retry_delay', 'timeout', 'identity_auth',
28+
'alias', 'num_retries', 'retry_delay', 'timeout', 'identity_auth',
2929
'proxy_host', 'proxy_port', 'proxy_user', 'proxy_password', 'proxy_pkey',
3030
'keepalive_seconds', 'ipv6_only', 'cert_file', 'auth_thread_pool', 'gssapi_auth',
3131
'gssapi_server_identity', 'gssapi_client_identity', 'gssapi_delegate_credentials',
3232
'forward_ssh_agent',
3333
)
3434

3535
def __init__(self, user=None, port=None, password=None, private_key=None,
36-
allow_agent=None, num_retries=None, retry_delay=None, timeout=None,
36+
allow_agent=None, alias=None, num_retries=None, retry_delay=None, timeout=None,
3737
identity_auth=None,
3838
proxy_host=None, proxy_port=None, proxy_user=None, proxy_password=None,
3939
proxy_pkey=None,
@@ -58,6 +58,8 @@ def __init__(self, user=None, port=None, password=None, private_key=None,
5858
:type private_key: str
5959
:param allow_agent: Enable/disable SSH agent authentication.
6060
:type allow_agent: bool
61+
:param alias: Use an alias for this host.
62+
:type alias: str or int
6163
:param num_retries: Number of retry attempts before giving up on connection
6264
and SSH operations.
6365
:type num_retries: int
@@ -103,6 +105,7 @@ def __init__(self, user=None, port=None, password=None, private_key=None,
103105
self.password = password
104106
self.private_key = private_key
105107
self.allow_agent = allow_agent
108+
self.alias = alias
106109
self.num_retries = num_retries
107110
self.timeout = timeout
108111
self.retry_delay = retry_delay
@@ -130,6 +133,8 @@ def _sanity_checks(self):
130133
raise ValueError("Port %s is not an integer" % (self.port,))
131134
if self.password is not None and not isinstance(self.password, str):
132135
raise ValueError("Password %s is not a string" % (self.password,))
136+
if self.alias is not None and not isinstance(self.alias, str):
137+
raise ValueError("Alias %s is not a string" % (self.alias,))
133138
if self.private_key is not None and not (
134139
isinstance(self.private_key, str) or isinstance(self.private_key, bytes)
135140
):

pssh/output.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ class HostOutput(object):
5555
"""Host output"""
5656

5757
__slots__ = ('host', 'channel', 'stdin',
58-
'client', 'exception', 'encoding', 'read_timeout',
59-
'buffers',
58+
'client', 'alias', 'exception',
59+
'encoding', 'read_timeout', 'buffers',
6060
)
6161

6262
def __init__(self, host, channel, stdin,
63-
client, exception=None, encoding='utf-8', read_timeout=None,
63+
client, alias=None, exception=None, encoding='utf-8', read_timeout=None,
6464
buffers=None):
6565
"""
6666
:param host: Host name output is for
@@ -71,6 +71,8 @@ def __init__(self, host, channel, stdin,
7171
:type stdin: :py:func:`file`-like object
7272
:param client: `SSHClient` output is coming from.
7373
:type client: :py:class:`pssh.clients.base.single.BaseSSHClient`
74+
:param alias: Host alias.
75+
:type alias: str
7476
:param exception: Exception from host if any
7577
:type exception: :py:class:`Exception` or ``None``
7678
:param read_timeout: Timeout in seconds for reading from buffers.
@@ -82,6 +84,7 @@ def __init__(self, host, channel, stdin,
8284
self.channel = channel
8385
self.stdin = stdin
8486
self.client = client
87+
self.alias = alias
8588
self.exception = exception
8689
self.encoding = encoding
8790
self.read_timeout = read_timeout
@@ -117,12 +120,13 @@ def exit_code(self):
117120

118121
def __repr__(self):
119122
return "\thost={host}{linesep}" \
123+
"\talias={alias}{linesep}" \
120124
"\texit_code={exit_code}{linesep}" \
121125
"\tchannel={channel}{linesep}" \
122126
"\texception={exception}{linesep}" \
123127
"\tencoding={encoding}{linesep}" \
124128
"\tread_timeout={read_timeout}".format(
125-
host=self.host, channel=self.channel,
129+
host=self.host, alias=self.alias, channel=self.channel,
126130
exception=self.exception, linesep=linesep,
127131
exit_code=self.exit_code, encoding=self.encoding, read_timeout=self.read_timeout,
128132
)

tests/native/test_parallel_client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,19 +930,22 @@ def test_host_config(self):
930930
servers = []
931931
password = 'overriden_pass'
932932
fake_key = 'FAKE KEY'
933+
aliases = [f"alias for host {host_i}" for host_i, _ in enumerate(hosts)]
933934
for host_i, (host, port) in enumerate(hosts):
934935
server = OpenSSHServer(listen_ip=host, port=port)
935936
server.start_server()
936937
host_config[host_i].port = port
937938
host_config[host_i].user = self.user
938939
host_config[host_i].password = password
939940
host_config[host_i].private_key = self.user_key
941+
host_config[host_i].alias = aliases[host_i]
940942
servers.append(server)
941943
host_config[1].private_key = fake_key
942944
client = ParallelSSHClient([h for h, _ in hosts],
943945
host_config=host_config,
944946
num_retries=1)
945947
output = client.run_command(self.cmd, stop_on_errors=False)
948+
946949
client.join(output)
947950
self.assertEqual(len(hosts), len(output))
948951
try:
@@ -954,6 +957,8 @@ def test_host_config(self):
954957
self.assertEqual(client._host_clients[0, hosts[0][0]].user, self.user)
955958
self.assertEqual(client._host_clients[0, hosts[0][0]].password, password)
956959
self.assertEqual(client._host_clients[0, hosts[0][0]].pkey, open(os.path.abspath(self.user_key), 'rb').read())
960+
self.assertEqual(set(aliases), set([client.alias for client in output]))
961+
957962
for server in servers:
958963
server.stop()
959964

tests/native/test_single_client.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ def test_execute(self):
180180
exit_code = host_out.channel.get_exit_status()
181181
self.assertEqual(host_out.exit_code, 0)
182182
self.assertEqual(expected, output)
183+
184+
def test_alias(self):
185+
client = SSHClient(self.host, port=self.port,
186+
pkey=self.user_key, num_retries=1,
187+
alias='test')
188+
host_out = client.run_command(self.cmd)
189+
self.assertEqual(host_out.alias, 'test')
183190

184191
def test_open_session_timeout(self):
185192
client = SSHClient(self.host, port=self.port,

tests/test_host_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def test_host_config_entries(self):
2626
user = 'user'
2727
port = 22
2828
password = 'password'
29+
alias = 'alias'
2930
private_key = 'private key'
3031
allow_agent = False
3132
num_retries = 1
@@ -43,7 +44,7 @@ def test_host_config_entries(self):
4344
gssapi_client_identity = 'some_id'
4445
gssapi_delegate_credentials = True
4546
cfg = HostConfig(
46-
user=user, port=port, password=password, private_key=private_key,
47+
user=user, port=port, password=password, alias=alias, private_key=private_key,
4748
allow_agent=allow_agent, num_retries=num_retries, retry_delay=retry_delay,
4849
timeout=timeout, identity_auth=identity_auth, proxy_host=proxy_host,
4950
ipv6_only=ipv6_only,
@@ -59,6 +60,7 @@ def test_host_config_entries(self):
5960
self.assertEqual(cfg.user, user)
6061
self.assertEqual(cfg.port, port)
6162
self.assertEqual(cfg.password, password)
63+
self.assertEqual(cfg.alias, alias)
6264
self.assertEqual(cfg.private_key, private_key)
6365
self.assertEqual(cfg.allow_agent, allow_agent)
6466
self.assertEqual(cfg.num_retries, num_retries)
@@ -79,6 +81,7 @@ def test_host_config_bad_entries(self):
7981
self.assertRaises(ValueError, HostConfig, user=22)
8082
self.assertRaises(ValueError, HostConfig, password=22)
8183
self.assertRaises(ValueError, HostConfig, port='22')
84+
self.assertRaises(ValueError, HostConfig, alias=2)
8285
self.assertRaises(ValueError, HostConfig, private_key=1)
8386
self.assertRaises(ValueError, HostConfig, allow_agent=1)
8487
self.assertRaises(ValueError, HostConfig, num_retries='')

0 commit comments

Comments
 (0)