Skip to content

Commit b86b1f2

Browse files
author
Pan
committed
Removed public key requirement from native clients. Added and updated tests.
Parallel clients raise type error on incorrect host list type. Native clients raise exception on init when private key path cannot be found. Added tests. Updated changelog, requirements.
1 parent 1515270 commit b86b1f2

File tree

11 files changed

+111
-54
lines changed

11 files changed

+111
-54
lines changed

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+
1.8.0
5+
++++++
6+
7+
Changes
8+
--------
9+
10+
* Native client no longer requires public key file for authentication.
11+
* Native clients raise ``pssh.exceptions.PKeyFileError`` on object initialisation if provided private key file paths cannot be found.
12+
* Native clients expand user directory (``~/<path>``) on provided private key paths.
13+
* Parallel clients raise ``TypeError`` when provided ``hosts`` is a string instead of list or other iterable.
14+
415
1.7.0
516
++++++
617

pssh/clients/base_pssh.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
4747
num_retries=DEFAULT_RETRIES,
4848
timeout=120, pool_size=10,
4949
host_config=None, retry_delay=RETRY_DELAY):
50+
if isinstance(hosts, str) or isinstance(hosts, bytes):
51+
raise TypeError(
52+
"Hosts must be list or other iterable, not string. "
53+
"For example: ['localhost'] not 'localhost'.")
5054
self.allow_agent = allow_agent
5155
self.pool_size = pool_size
5256
self.pool = gevent.pool.Pool(size=self.pool_size)

pssh/clients/native/common.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# This file is part of parallel-ssh.
2+
#
3+
# Copyright (C) 2014-2018 Panos Kittenis.
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation, version 2.1.
8+
#
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library; if not, write to the Free Software
16+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17+
18+
import os
19+
20+
from ...exceptions import PKeyFileError
21+
22+
23+
def _validate_pkey_path(pkey, host=None):
24+
if pkey is None:
25+
return
26+
pkey = os.path.normpath(os.path.expanduser(pkey))
27+
if not os.path.exists(pkey):
28+
msg = "File %s does not exist. " \
29+
"Please use either absolute or relative to user directory " \
30+
"paths like '~/.ssh/my_key' for pkey parameter"
31+
ex = PKeyFileError(msg, pkey)
32+
ex.host = host
33+
raise ex
34+
return pkey

pssh/clients/native/parallel.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .single import SSHClient
2626
from ...exceptions import ProxyError, Timeout, HostArgumentException
2727
from .tunnel import Tunnel
28+
from .common import _validate_pkey_path
2829

2930

3031
logger = logging.getLogger(__name__)
@@ -50,8 +51,8 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
5051
:param port: (Optional) Port number to use for SSH connection. Defaults
5152
to 22.
5253
:type port: int
53-
:param pkey: Private key file path to use. Note that the public key file
54-
pair *must* also exist in the same location with name ``<pkey>.pub``
54+
:param pkey: Private key file path to use. Path must be either absolute
55+
path or relative to user home directory like ``~/<path>``.
5556
:type pkey: str
5657
:param num_retries: (Optional) Number of connection and authentication
5758
attempts before the client gives up. Defaults to 3.
@@ -90,25 +91,27 @@ def __init__(self, hosts, user=None, password=None, port=22, pkey=None,
9091
:param proxy_pkey: (Optional) Private key file to be used for
9192
authentication with ``proxy_host``. Defaults to available keys from
9293
SSHAgent and user's SSH identities.
93-
:type proxy_pkey: Private key file path to use. Note that the public
94-
key file pair *must* also exist in the same location with name
95-
``<pkey>.pub``.
94+
:type proxy_pkey: Private key file path to use.
9695
:param forward_ssh_agent: (Optional) Turn on SSH agent forwarding -
9796
equivalent to `ssh -A` from the `ssh` command line utility.
9897
Defaults to True if not set.
9998
:type forward_ssh_agent: bool
10099
:param tunnel_timeout: (Optional) Timeout setting for proxy tunnel
101100
connections.
102101
:type tunnel_timeout: float
102+
103+
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
104+
provided private key.
103105
"""
104106
BaseParallelSSHClient.__init__(
105107
self, hosts, user=user, password=password, port=port, pkey=pkey,
106108
allow_agent=allow_agent, num_retries=num_retries,
107109
timeout=timeout, pool_size=pool_size,
108110
host_config=host_config, retry_delay=retry_delay)
111+
self.pkey = _validate_pkey_path(pkey)
109112
self.proxy_host = proxy_host
110113
self.proxy_port = proxy_port
111-
self.proxy_pkey = proxy_pkey
114+
self.proxy_pkey = _validate_pkey_path(proxy_pkey)
112115
self.proxy_user = proxy_user
113116
self.proxy_password = proxy_password
114117
self.forward_ssh_agent = forward_ssh_agent

pssh/clients/native/single.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
SCPError
4242
from ...constants import DEFAULT_RETRIES, RETRY_DELAY
4343
from ...native._ssh2 import wait_select, _read_output # , sftp_get, sftp_put
44+
from .common import _validate_pkey_path
4445

4546

4647
Hub.NOT_ERROR = (Exception,)
@@ -75,9 +76,9 @@ def __init__(self, host,
7576
:type password: str
7677
:param port: SSH port to connect to. Defaults to SSH default (22)
7778
:type port: int
78-
:param pkey: Private key file path to use for authentication.
79-
Note that the public key file
80-
pair *must* also exist in the same location with name ``<pkey>.pub``
79+
:param pkey: Private key file path to use for authentication. Path must
80+
be either absolute path or relative to user home directory
81+
like ``~/<path>``.
8182
:type pkey: str
8283
:param num_retries: (Optional) Number of connection and authentication
8384
attempts before the client gives up. Defaults to 3.
@@ -98,6 +99,9 @@ def __init__(self, host,
9899
:param proxy_host: Connection to host is via provided proxy host
99100
and client should use self.proxy_host for connection attempts.
100101
:type proxy_host: str
102+
103+
:raises: :py:class:`pssh.exceptions.PKeyFileError` on errors finding
104+
provided private key.
101105
"""
102106
self.host = host
103107
self.user = user if user else None
@@ -107,7 +111,6 @@ def __init__(self, host,
107111
raise ValueError("Must provide user parameter on Windows")
108112
self.password = password
109113
self.port = port if port else 22
110-
self.pkey = pkey
111114
self.num_retries = num_retries
112115
self.sock = None
113116
self.timeout = timeout * 1000 if timeout else None
@@ -117,6 +120,7 @@ def __init__(self, host,
117120
self._forward_requested = False
118121
self.session = None
119122
self._host = proxy_host if proxy_host else host
123+
self.pkey = _validate_pkey_path(pkey, self.host)
120124
self._connect(self._host, self.port)
121125
if _auth_thread_pool:
122126
THREAD_POOL.apply(self._init)
@@ -130,7 +134,7 @@ def disconnect(self):
130134
self._eagain(self.session.disconnect)
131135
except Exception:
132136
pass
133-
if not self.sock.closed:
137+
if self.sock is not None and not self.sock.closed:
134138
self.sock.close()
135139

136140
def __del__(self):
@@ -202,29 +206,24 @@ def _connect(self, host, port, retries=1):
202206
self.num_retries,)
203207

204208
def _pkey_auth(self):
205-
pub_file = "%s.pub" % self.pkey
206-
logger.debug("Attempting authentication with public key %s for user %s",
207-
pub_file, self.user)
208209
self.session.userauth_publickey_fromfile(
209210
self.user,
210-
pub_file,
211211
self.pkey,
212-
self.password if self.password is not None else '')
212+
passphrase=self.password if self.password is not None else '')
213213

214214
def _identity_auth(self):
215+
passphrase = self.password if self.password is not None else ''
215216
for identity_file in self.IDENTITIES:
216217
if not os.path.isfile(identity_file):
217218
continue
218-
pub_file = "%s.pub" % (identity_file)
219219
logger.debug(
220220
"Trying to authenticate with identity file %s",
221221
identity_file)
222222
try:
223223
self.session.userauth_publickey_fromfile(
224224
self.user,
225-
pub_file,
226225
identity_file,
227-
self.password if self.password is not None else '')
226+
passphrase=passphrase)
228227
except Exception:
229228
logger.debug("Authentication with identity file %s failed, "
230229
"continuing with other identities",
@@ -239,7 +238,7 @@ def _identity_auth(self):
239238
def auth(self):
240239
if self.pkey is not None:
241240
logger.debug(
242-
"Proceeding with public key file authentication")
241+
"Proceeding with private key file authentication")
243242
return self._pkey_auth()
244243
if self.allow_agent:
245244
try:
@@ -256,11 +255,13 @@ def auth(self):
256255
except AuthenticationException:
257256
if self.password is None:
258257
raise
259-
logger.debug("Public key auth failed, trying password")
258+
logger.debug("Private key auth failed, trying password")
260259
self._password_auth()
261260

262261
def _password_auth(self):
263-
if self.session.userauth_password(self.user, self.password) != 0:
262+
try:
263+
self.session.userauth_password(self.user, self.password)
264+
except Exception:
264265
raise AuthenticationException("Password authentication failed")
265266

266267
def open_session(self):

pssh/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ class Timeout(Exception):
6969

7070
class SCPError(Exception):
7171
"""Raised on errors copying file via SCP"""
72+
73+
74+
class PKeyFileError(Exception):
75+
"""Raised on errors finding private key file"""

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
paramiko>=1.15.3,>=2.4
22
gevent>=1.1
3-
ssh2-python>=0.12.0
3+
ssh2-python>=0.15.0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
'tests', 'tests.*',
9797
'*.tests', '*.tests.*')
9898
),
99-
install_requires=['paramiko', gevent_req, 'ssh2-python>=0.14.0'],
99+
install_requires=['paramiko', gevent_req, 'ssh2-python>=0.15.0'],
100100
classifiers=[
101101
'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)',
102102
'Intended Audience :: Developers',

tests/test_native_parallel_client.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from pssh.exceptions import UnknownHostException, \
4040
AuthenticationException, ConnectionErrorException, SessionError, \
4141
HostArgumentException, SFTPError, SFTPIOError, Timeout, SCPError, \
42-
ProxyError
42+
ProxyError, PKeyFileError
4343
from pssh import logger as pssh_logger
4444

4545
from .embedded_server.embedded_server import make_socket
@@ -833,36 +833,19 @@ def test_connection_error_exception(self):
833833
try:
834834
raise output[host]['exception']
835835
except ConnectionErrorException as ex:
836-
self.assertEqual(ex.args[1], host,
836+
self.assertEqual(ex.host, host,
837837
msg="Exception host argument is %s, should be %s" % (
838-
ex.args[1], host,))
838+
ex.host, host,))
839839
self.assertEqual(ex.args[2], port,
840840
msg="Exception port argument is %s, should be %s" % (
841841
ex.args[2], port,))
842842
else:
843843
raise Exception("Expected ConnectionErrorException")
844844

845-
def test_authentication_exception(self):
846-
"""Test that we get authentication exception in output with correct arguments"""
847-
hosts = [self.host]
848-
client = ParallelSSHClient(hosts, port=self.port,
849-
pkey='A REALLY FAKE KEY',
850-
num_retries=1)
851-
output = client.run_command(self.cmd, stop_on_errors=False)
852-
self.assertTrue('exception' in output[self.host],
853-
msg="Got no exception for host %s - expected connection error" % (
854-
self.host,))
855-
try:
856-
raise output[self.host]['exception']
857-
except AuthenticationException as ex:
858-
self.assertEqual(ex.args[1], self.host,
859-
msg="Exception host argument is %s, should be %s" % (
860-
ex.args[1], self.host,))
861-
self.assertEqual(ex.args[2], self.port,
862-
msg="Exception port argument is %s, should be %s" % (
863-
ex.args[2], self.port,))
864-
else:
865-
raise Exception("Expected AuthenticationException")
845+
def test_bad_pkey_path(self):
846+
self.assertRaises(PKeyFileError, ParallelSSHClient, [self.host], port=self.port,
847+
pkey='A REALLY FAKE KEY',
848+
num_retries=1)
866849

867850
def test_multiple_single_quotes_in_cmd(self):
868851
"""Test that we can run a command with multiple single quotes"""
@@ -937,10 +920,10 @@ def test_host_config(self):
937920
self.assertTrue(host in output)
938921
try:
939922
raise output[hosts[1][0]]['exception']
940-
except AuthenticationException as ex:
941-
pass
923+
except PKeyFileError as ex:
924+
self.assertEqual(ex.host, host)
942925
else:
943-
raise AssertionError("Expected AutnenticationException on host %s",
926+
raise AssertionError("Expected ValueError on host %s",
944927
hosts[0][0])
945928
self.assertTrue(output[hosts[1][0]].exit_code is None,
946929
msg="Execution failed on host %s" % (hosts[1][0],))
@@ -1396,3 +1379,7 @@ def test_scp_recv(self):
13961379
finally:
13971380
shutil.rmtree(remote_test_path_abs)
13981381
shutil.rmtree(local_copied_dir)
1382+
1383+
def test_bad_hosts_value(self):
1384+
self.assertRaises(TypeError, ParallelSSHClient, 'a host')
1385+
self.assertRaises(TypeError, ParallelSSHClient, b'a host')

tests/test_native_single_client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ssh2.session import Session
1313
from ssh2.exceptions import SocketDisconnectError
1414
from pssh.exceptions import AuthenticationException, ConnectionErrorException, \
15-
SessionError, SFTPIOError, SFTPError, SCPError
15+
SessionError, SFTPIOError, SFTPError, SCPError, PKeyFileError
1616

1717

1818
ssh_logger.setLevel(logging.DEBUG)
@@ -91,6 +91,14 @@ def test_manual_auth(self):
9191
client.session.handshake(client.sock)
9292
self.assertRaises(AuthenticationException, client.auth)
9393

94+
def test_failed_auth(self):
95+
self.assertRaises(PKeyFileError, SSHClient, self.host, port=self.port,
96+
pkey='client_pkey',
97+
num_retries=1)
98+
self.assertRaises(PKeyFileError, SSHClient, self.host, port=self.port,
99+
pkey='~/fake_key',
100+
num_retries=1)
101+
94102
def test_handshake_fail(self):
95103
client = SSHClient(self.host, port=self.port,
96104
pkey=self.user_key,

0 commit comments

Comments
 (0)