xref: /dpdk/dts/framework/remote_session/interactive_remote_session.py (revision 4227446306a0b97826d587ef6f0d7ecc054af781)
1840b1e01SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
2840b1e01SJuraj Linkeš# Copyright(c) 2023 University of New Hampshire
3840b1e01SJuraj Linkeš
4840b1e01SJuraj Linkeš"""Handler for an SSH session dedicated to interactive shells."""
5840b1e01SJuraj Linkeš
6840b1e01SJuraj Linkešimport socket
7840b1e01SJuraj Linkešimport traceback
8*42274463SJuraj Linkešfrom typing import Union
9840b1e01SJuraj Linkeš
10282688eaSLuca Vizzarrofrom paramiko import AutoAddPolicy, SSHClient, Transport  # type: ignore[import-untyped]
11282688eaSLuca Vizzarrofrom paramiko.ssh_exception import (  # type: ignore[import-untyped]
12840b1e01SJuraj Linkeš    AuthenticationException,
13840b1e01SJuraj Linkeš    BadHostKeyException,
14840b1e01SJuraj Linkeš    NoValidConnectionsError,
15840b1e01SJuraj Linkeš    SSHException,
16840b1e01SJuraj Linkeš)
17840b1e01SJuraj Linkeš
18840b1e01SJuraj Linkešfrom framework.config import NodeConfiguration
19840b1e01SJuraj Linkešfrom framework.exception import SSHConnectionError
2004f5a5a6SJuraj Linkešfrom framework.logger import DTSLogger
21840b1e01SJuraj Linkeš
22840b1e01SJuraj Linkeš
23840b1e01SJuraj Linkešclass InteractiveRemoteSession:
24840b1e01SJuraj Linkeš    """SSH connection dedicated to interactive applications.
25840b1e01SJuraj Linkeš
266ef07151SJuraj Linkeš    The connection is created using `paramiko <https://docs.paramiko.org/en/latest/>`_
276ef07151SJuraj Linkeš    and is a persistent connection to the host. This class defines the methods for connecting
286ef07151SJuraj Linkeš    to the node and configures the connection to send "keep alive" packets every 30 seconds.
296ef07151SJuraj Linkeš    Because paramiko attempts to use SSH keys to establish a connection first, providing
306ef07151SJuraj Linkeš    a password is optional. This session is utilized by InteractiveShells
316ef07151SJuraj Linkeš    and cannot be interacted with directly.
32840b1e01SJuraj Linkeš
33840b1e01SJuraj Linkeš    Attributes:
346ef07151SJuraj Linkeš        hostname: The hostname that will be used to initialize a connection to the node.
356ef07151SJuraj Linkeš        ip: A subsection of `hostname` that removes the port for the connection if there
36840b1e01SJuraj Linkeš            is one. If there is no port, this will be the same as hostname.
376ef07151SJuraj Linkeš        port: Port to use for the ssh connection. This will be extracted from `hostname`
386ef07151SJuraj Linkeš            if there is a port included, otherwise it will default to ``22``.
39840b1e01SJuraj Linkeš        username: User to connect to the node with.
40840b1e01SJuraj Linkeš        password: Password of the user connecting to the host. This will default to an
41840b1e01SJuraj Linkeš            empty string if a password is not provided.
426ef07151SJuraj Linkeš        session: The underlying paramiko connection.
43840b1e01SJuraj Linkeš
44840b1e01SJuraj Linkeš    Raises:
45840b1e01SJuraj Linkeš        SSHConnectionError: There is an error creating the SSH connection.
46840b1e01SJuraj Linkeš    """
47840b1e01SJuraj Linkeš
48840b1e01SJuraj Linkeš    hostname: str
49840b1e01SJuraj Linkeš    ip: str
50840b1e01SJuraj Linkeš    port: int
51840b1e01SJuraj Linkeš    username: str
52840b1e01SJuraj Linkeš    password: str
53840b1e01SJuraj Linkeš    session: SSHClient
5404f5a5a6SJuraj Linkeš    _logger: DTSLogger
55840b1e01SJuraj Linkeš    _node_config: NodeConfiguration
56*42274463SJuraj Linkeš    _transport: Union[Transport, None]
57840b1e01SJuraj Linkeš
5804f5a5a6SJuraj Linkeš    def __init__(self, node_config: NodeConfiguration, logger: DTSLogger) -> None:
596ef07151SJuraj Linkeš        """Connect to the node during initialization.
606ef07151SJuraj Linkeš
616ef07151SJuraj Linkeš        Args:
626ef07151SJuraj Linkeš            node_config: The test run configuration of the node to connect to.
636ef07151SJuraj Linkeš            logger: The logger instance this session will use.
646ef07151SJuraj Linkeš        """
65840b1e01SJuraj Linkeš        self._node_config = node_config
666ef07151SJuraj Linkeš        self._logger = logger
67840b1e01SJuraj Linkeš        self.hostname = node_config.hostname
68840b1e01SJuraj Linkeš        self.username = node_config.user
69840b1e01SJuraj Linkeš        self.password = node_config.password if node_config.password else ""
70840b1e01SJuraj Linkeš        port = "22"
71840b1e01SJuraj Linkeš        self.ip = node_config.hostname
72840b1e01SJuraj Linkeš        if ":" in node_config.hostname:
73840b1e01SJuraj Linkeš            self.ip, port = node_config.hostname.split(":")
74840b1e01SJuraj Linkeš        self.port = int(port)
75840b1e01SJuraj Linkeš        self._logger.info(
76840b1e01SJuraj Linkeš            f"Initializing interactive connection for {self.username}@{self.hostname}"
77840b1e01SJuraj Linkeš        )
78840b1e01SJuraj Linkeš        self._connect()
79840b1e01SJuraj Linkeš        self._logger.info(f"Interactive connection successful for {self.username}@{self.hostname}")
80840b1e01SJuraj Linkeš
81840b1e01SJuraj Linkeš    def _connect(self) -> None:
82840b1e01SJuraj Linkeš        """Establish a connection to the node.
83840b1e01SJuraj Linkeš
84840b1e01SJuraj Linkeš        Connection attempts can be retried up to 10 times if certain errors are
85840b1e01SJuraj Linkeš        encountered (NoValidConnectionsError, socket.error, SSHException). If a
86840b1e01SJuraj Linkeš        connection is made, a 30 second "keep alive" interval will be set on the
87840b1e01SJuraj Linkeš        session.
88840b1e01SJuraj Linkeš
89840b1e01SJuraj Linkeš        Raises:
90840b1e01SJuraj Linkeš            SSHConnectionError: Connection cannot be established.
91840b1e01SJuraj Linkeš        """
92840b1e01SJuraj Linkeš        client = SSHClient()
93840b1e01SJuraj Linkeš        client.set_missing_host_key_policy(AutoAddPolicy)
94840b1e01SJuraj Linkeš        self.session = client
95840b1e01SJuraj Linkeš        retry_attempts = 10
96840b1e01SJuraj Linkeš        for retry_attempt in range(retry_attempts):
97840b1e01SJuraj Linkeš            try:
98840b1e01SJuraj Linkeš                client.connect(
99840b1e01SJuraj Linkeš                    self.ip,
100840b1e01SJuraj Linkeš                    username=self.username,
101840b1e01SJuraj Linkeš                    port=self.port,
102840b1e01SJuraj Linkeš                    password=self.password,
103840b1e01SJuraj Linkeš                    timeout=20 if self.port else 10,
104840b1e01SJuraj Linkeš                )
105840b1e01SJuraj Linkeš            except (TypeError, BadHostKeyException, AuthenticationException) as e:
106840b1e01SJuraj Linkeš                self._logger.exception(e)
107840b1e01SJuraj Linkeš                raise SSHConnectionError(self.hostname) from e
108840b1e01SJuraj Linkeš            except (NoValidConnectionsError, socket.error, SSHException) as e:
109840b1e01SJuraj Linkeš                self._logger.debug(traceback.format_exc())
110840b1e01SJuraj Linkeš                self._logger.warning(e)
111840b1e01SJuraj Linkeš                self._logger.info(
112840b1e01SJuraj Linkeš                    f"Retrying interactive session connection: retry number {retry_attempt +1}"
113840b1e01SJuraj Linkeš                )
114840b1e01SJuraj Linkeš            else:
115840b1e01SJuraj Linkeš                break
116840b1e01SJuraj Linkeš        else:
117840b1e01SJuraj Linkeš            raise SSHConnectionError(self.hostname)
118840b1e01SJuraj Linkeš        # Interactive sessions are used on an "as needed" basis so we have
119840b1e01SJuraj Linkeš        # to set a keepalive
120840b1e01SJuraj Linkeš        self._transport = self.session.get_transport()
121840b1e01SJuraj Linkeš        if self._transport is not None:
122840b1e01SJuraj Linkeš            self._transport.set_keepalive(30)
123