From f45ad8107b4ad179746c6c3770042ed89fe0236b Mon Sep 17 00:00:00 2001 From: Melissa Date: Tue, 20 Jul 2021 15:56:32 -0400 Subject: [PATCH] mgr/cephadm: create and cache asyncssh connection objects, and handle asyncssh connection errors Create asyncssh connection object in async `_remote_connection` function and cache in `self.cons` Create a handler for asyncssh log redirection and output ssh log if a connection error occurs Disable asyncssh logger from propagating because the asyncssh info messages are verbose Fixes: https://tracker.ceph.com/issues/44676 Signed-off-by: Melissa Li --- src/pybind/mgr/cephadm/ssh.py | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/pybind/mgr/cephadm/ssh.py diff --git a/src/pybind/mgr/cephadm/ssh.py b/src/pybind/mgr/cephadm/ssh.py new file mode 100644 index 0000000000000..8eeeadf2993c5 --- /dev/null +++ b/src/pybind/mgr/cephadm/ssh.py @@ -0,0 +1,90 @@ +import logging +from contextlib import contextmanager +from io import StringIO +from typing import TYPE_CHECKING, Optional, List, Tuple, Dict, Any, Iterator +from orchestrator import OrchestratorError + +try: + import asyncssh +except ImportError: + asyncssh = None + +if TYPE_CHECKING: + from cephadm.module import CephadmOrchestrator + from asyncssh.connection import SSHClientConnection + +logger = logging.getLogger(__name__) + +asyncssh_logger = logging.getLogger('asyncssh') +asyncssh_logger.propagate = False + +class SSHManager: + + def __init__(self, mgr: "CephadmOrchestrator"): + self.mgr: "CephadmOrchestrator" = mgr + self.cons: Dict[str, "SSHClientConnection"] = {} + + async def _remote_connection(self, + host: str, + addr: Optional[str] = None, + ) -> "SSHClientConnection": + if not self.cons.get(host): + if not addr and host in self.mgr.inventory: + addr = self.mgr.inventory.get_addr(host) + + if not addr: + raise OrchestratorError("host address is empty") + + assert self.mgr.ssh_user + n = self.mgr.ssh_user + '@' + addr + logger.debug("Opening connection to {} with ssh options '{}'".format( + n, self.mgr._ssh_options)) + + asyncssh.set_log_level('DEBUG') + asyncssh.set_debug_level(3) + + with self.redirect_log(host, addr): + try: + conn = await asyncssh.connect(addr, username=self.mgr.ssh_user, client_keys=[self.mgr.tkey.name], known_hosts=None, config=[self.mgr.ssh_config_fname], preferred_auth=['publickey']) + except OSError: + raise + except asyncssh.Error: + raise + except Exception: + raise + self.cons[host] = conn + + self.mgr.offline_hosts_remove(host) + conn = self.cons.get(host) + return conn + + @contextmanager + def redirect_log(self, host: str, addr: str) -> Iterator[None]: + log_string = StringIO() + ch = logging.StreamHandler(log_string) + ch.setLevel(logging.DEBUG) + asyncssh_logger.addHandler(ch) + + try: + yield + except OSError as e: + self.mgr.offline_hosts.add(host) + log_content = log_string.getvalue() + msg = f"Can't communicate with remote host `{addr}`, possibly because python3 is not installed there. {str(e)}" + '\n' + f'Log: {log_content}' + logger.exception(msg) + raise OrchestratorError(msg) + except asyncssh.Error as e: + self.mgr.offline_hosts.add(host) + log_content = log_string.getvalue() + msg = f'Failed to connect to {host} ({addr}). {str(e)}' + '\n' + f'Log: {log_content}' + logger.debug(msg) + raise OrchestratorError(msg) + except Exception as e: + self.mgr.offline_hosts.add(host) + log_content = log_string.getvalue() + logger.exception(str(e)) + raise OrchestratorError(f'Failed to connect to {host} ({addr}): {repr(e)}' + '\n' f'Log: {log_content}') + finally: + log_string.flush() + asyncssh_logger.removeHandler(ch) + -- 2.39.5