diff --git a/tobiko/__init__.py b/tobiko/__init__.py index 6a1a13cd4..10eb5e26f 100644 --- a/tobiko/__init__.py +++ b/tobiko/__init__.py @@ -59,6 +59,7 @@ tobiko_config_path = _config.tobiko_config_path TobikoException = _exception.TobikoException check_valid_type = _exception.check_valid_type exc_info = _exception.exc_info +ExceptionInfo = _exception.ExceptionInfo handle_multiple_exceptions = _exception.handle_multiple_exceptions list_exc_infos = _exception.list_exc_infos diff --git a/tobiko/shell/ssh/_client.py b/tobiko/shell/ssh/_client.py index 39e8f78c4..66a6e3af1 100644 --- a/tobiko/shell/ssh/_client.py +++ b/tobiko/shell/ssh/_client.py @@ -18,7 +18,6 @@ from __future__ import absolute_import from collections import abc import contextlib import getpass -import io import os import subprocess import time @@ -26,6 +25,7 @@ import threading import typing import netaddr +import testtools from oslo_log import log import paramiko from paramiko import common @@ -578,32 +578,63 @@ def ssh_client(host, port=None, username=None, proxy_jump=None, **connect_parameters) -def load_private_keys(key_filenames: typing.List[str]) \ +KEY_CLASSES: typing.List[typing.Type[paramiko.PKey]] = [ + paramiko.RSAKey, + paramiko.DSSKey, + paramiko.ECDSAKey, + paramiko.Ed25519Key, +] + + +def load_private_keys(key_filenames: typing.List[str], + password: str = None) \ -> typing.List[paramiko.PKey]: pkeys: typing.List[paramiko.PKey] = [] for filename in key_filenames: if os.path.exists(filename): try: - with io.open(filename, 'rt') as fd: - pkey: paramiko.PKey = paramiko.RSAKey.from_private_key(fd) - except Exception: - LOG.error('Unable to get RSAKey private key from file: ' - f'{filename}', exc_info=1) + pkey = load_private_key(filename, password=password) + except LoadPrivateKeyError as ex: + LOG.exception(f'Error loading key file: {ex}') else: pkeys.append(pkey) + else: + LOG.debug(f'Key file not found: {filename}') return pkeys +class LoadPrivateKeyError(tobiko.TobikoException): + message = "Unable to load private key from file {filename}" + + +def load_private_key(filename: str, + password: str = None) -> paramiko.PKey: + errors: typing.List[tobiko.ExceptionInfo] = [] + for key_class in KEY_CLASSES: + try: + pkey: paramiko.PKey = key_class.from_private_key_file( + filename=filename, password=password) + except (paramiko.SSHException, ValueError): + errors.append(tobiko.exc_info()) + else: + LOG.debug(f'Key file loaded: {filename} (key_class={key_class})') + return pkey + + cause = testtools.MultipleExceptions(*errors) + raise LoadPrivateKeyError(filename=filename) from cause + + def ssh_connect(hostname, username=None, port=None, connection_interval=None, connection_attempts=None, connection_timeout=None, proxy_command=None, proxy_client=None, key_filename=None, + password: str = None, **parameters): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.WarningPolicy()) login = _command.ssh_login(hostname=hostname, username=username, port=port) assert isinstance(key_filename, list) - pkeys = load_private_keys(key_filename) + pkeys = load_private_keys(key_filename, password=password) auth_failed: typing.Optional[Exception] = None for attempt in tobiko.retry(count=connection_attempts, timeout=connection_timeout, @@ -614,7 +645,7 @@ def ssh_connect(hostname, username=None, port=None, connection_interval=None, LOG.debug(f"Logging in to '{login}'...\n" f" - parameters: {parameters}\n" f" - attempt: {attempt.details}\n") - for pkey in pkeys + [None]: + for pkey in pkeys + [None]: # type: ignore succeeded = False proxy_sock = ssh_proxy_sock( hostname=hostname, diff --git a/tobiko/shell/ssh/_ssh_key_file.py b/tobiko/shell/ssh/_ssh_key_file.py index 2df9d4527..859c58af6 100644 --- a/tobiko/shell/ssh/_ssh_key_file.py +++ b/tobiko/shell/ssh/_ssh_key_file.py @@ -15,6 +15,7 @@ # under the License. from __future__ import absolute_import +import os.path import typing # noqa from oslo_log import log @@ -75,7 +76,8 @@ class GetSSHKeyFileFixture(tobiko.SharedFixture): return key_file = tobiko.tobiko_config_path( - f"~/.ssh/id_rsa-{remote_hostname}") + f"~/.ssh/{os.path.basename(self.remote_key_file)}-" + + remote_hostname) with tobiko.open_output_file(key_file) as fd: fd.write(private_key.decode()) with tobiko.open_output_file(key_file + '.pub') as fd: diff --git a/tobiko/shell/ssh/config.py b/tobiko/shell/ssh/config.py index 19c4e6b71..adef6e29b 100644 --- a/tobiko/shell/ssh/config.py +++ b/tobiko/shell/ssh/config.py @@ -40,8 +40,12 @@ OPTIONS = [ default=['ssh_config', '.ssh/config'], help="Default user SSH configuration files"), cfg.ListOpt('key_file', - default=['~/.ssh/id_rsa', '.ssh/id'], - help="Default SSH private key file(s)"), + default=['.ssh/id', + '~/.ssh/id_dsa', + '~/.ssh/id_rsa', + '~/.ssh/id_ecdsa', + '~/.ssh/id_ed25519'], + help="Default SSH private key file(s) wildcard"), cfg.BoolOpt('allow_agent', default=False, help=("Set to False to disable connecting to the " @@ -97,7 +101,10 @@ def setup_tobiko_config(conf): ssh_proxy_client = _client.ssh_proxy_client() if ssh_proxy_client: - key_file = _ssh_key_file.get_key_file(ssh_client=ssh_proxy_client) - if key_file and os.path.isfile(key_file): - LOG.info(f"Use SSH proxy server keyfile: {key_file}") - conf.ssh.key_file.append(key_file) + key_file: str + for remote_key_file in conf.ssh.key_file: + key_file = _ssh_key_file.get_key_file(ssh_client=ssh_proxy_client, + key_file=remote_key_file) + if key_file and os.path.isfile(key_file): + LOG.info(f"Use SSH proxy server keyfile: {key_file}") + conf.ssh.key_file.append(key_file)