xref: /dpdk/dts/framework/remote_session/ssh_session.py (revision 441c5fbf939b55f635d42ad9b7dcc3741e1c2a7c)
1840b1e01SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
2840b1e01SJuraj Linkeš# Copyright(c) 2023 PANTHEON.tech s.r.o.
3840b1e01SJuraj Linkeš
46ef07151SJuraj Linkeš"""SSH remote session."""
56ef07151SJuraj Linkeš
6840b1e01SJuraj Linkešimport socket
7840b1e01SJuraj Linkešimport traceback
8*441c5fbfSTomáš Ďurovecfrom pathlib import Path, PurePath
9840b1e01SJuraj Linkeš
10282688eaSLuca Vizzarrofrom fabric import Connection  # type: ignore[import-untyped]
11282688eaSLuca Vizzarrofrom invoke.exceptions import (  # type: ignore[import-untyped]
12840b1e01SJuraj Linkeš    CommandTimedOut,
13840b1e01SJuraj Linkeš    ThreadException,
14840b1e01SJuraj Linkeš    UnexpectedExit,
15840b1e01SJuraj Linkeš)
16282688eaSLuca Vizzarrofrom paramiko.ssh_exception import (  # type: ignore[import-untyped]
17840b1e01SJuraj Linkeš    AuthenticationException,
18840b1e01SJuraj Linkeš    BadHostKeyException,
19840b1e01SJuraj Linkeš    NoValidConnectionsError,
20840b1e01SJuraj Linkeš    SSHException,
21840b1e01SJuraj Linkeš)
22840b1e01SJuraj Linkeš
23840b1e01SJuraj Linkešfrom framework.exception import SSHConnectionError, SSHSessionDeadError, SSHTimeoutError
24840b1e01SJuraj Linkeš
25840b1e01SJuraj Linkešfrom .remote_session import CommandResult, RemoteSession
26840b1e01SJuraj Linkeš
27840b1e01SJuraj Linkeš
28840b1e01SJuraj Linkešclass SSHSession(RemoteSession):
29840b1e01SJuraj Linkeš    """A persistent SSH connection to a remote Node.
30840b1e01SJuraj Linkeš
316ef07151SJuraj Linkeš    The connection is implemented with
326ef07151SJuraj Linkeš    `the Fabric Python library <https://docs.fabfile.org/en/latest/>`_.
33840b1e01SJuraj Linkeš
34840b1e01SJuraj Linkeš    Attributes:
35840b1e01SJuraj Linkeš        session: The underlying Fabric SSH connection.
36840b1e01SJuraj Linkeš
37840b1e01SJuraj Linkeš    Raises:
38840b1e01SJuraj Linkeš        SSHConnectionError: The connection cannot be established.
39840b1e01SJuraj Linkeš    """
40840b1e01SJuraj Linkeš
41840b1e01SJuraj Linkeš    session: Connection
42840b1e01SJuraj Linkeš
43840b1e01SJuraj Linkeš    def _connect(self) -> None:
44840b1e01SJuraj Linkeš        errors = []
45840b1e01SJuraj Linkeš        retry_attempts = 10
46840b1e01SJuraj Linkeš        login_timeout = 20 if self.port else 10
47840b1e01SJuraj Linkeš        for retry_attempt in range(retry_attempts):
48840b1e01SJuraj Linkeš            try:
49840b1e01SJuraj Linkeš                self.session = Connection(
50840b1e01SJuraj Linkeš                    self.ip,
51840b1e01SJuraj Linkeš                    user=self.username,
52840b1e01SJuraj Linkeš                    port=self.port,
53840b1e01SJuraj Linkeš                    connect_kwargs={"password": self.password},
54840b1e01SJuraj Linkeš                    connect_timeout=login_timeout,
55840b1e01SJuraj Linkeš                )
56840b1e01SJuraj Linkeš                self.session.open()
57840b1e01SJuraj Linkeš
58840b1e01SJuraj Linkeš            except (ValueError, BadHostKeyException, AuthenticationException) as e:
59840b1e01SJuraj Linkeš                self._logger.exception(e)
60840b1e01SJuraj Linkeš                raise SSHConnectionError(self.hostname) from e
61840b1e01SJuraj Linkeš
62840b1e01SJuraj Linkeš            except (NoValidConnectionsError, socket.error, SSHException) as e:
63840b1e01SJuraj Linkeš                self._logger.debug(traceback.format_exc())
64840b1e01SJuraj Linkeš                self._logger.warning(e)
65840b1e01SJuraj Linkeš
66840b1e01SJuraj Linkeš                error = repr(e)
67840b1e01SJuraj Linkeš                if error not in errors:
68840b1e01SJuraj Linkeš                    errors.append(error)
69840b1e01SJuraj Linkeš
70840b1e01SJuraj Linkeš                self._logger.info(f"Retrying connection: retry number {retry_attempt + 1}.")
71840b1e01SJuraj Linkeš
72840b1e01SJuraj Linkeš            else:
73840b1e01SJuraj Linkeš                break
74840b1e01SJuraj Linkeš        else:
75840b1e01SJuraj Linkeš            raise SSHConnectionError(self.hostname, errors)
76840b1e01SJuraj Linkeš
77840b1e01SJuraj Linkeš    def _send_command(self, command: str, timeout: float, env: dict | None) -> CommandResult:
78840b1e01SJuraj Linkeš        """Send a command and return the result of the execution.
79840b1e01SJuraj Linkeš
80840b1e01SJuraj Linkeš        Args:
81840b1e01SJuraj Linkeš            command: The command to execute.
826ef07151SJuraj Linkeš            timeout: Wait at most this long in seconds for the command execution to complete.
83840b1e01SJuraj Linkeš            env: Extra environment variables that will be used in command execution.
84840b1e01SJuraj Linkeš
85840b1e01SJuraj Linkeš        Raises:
86840b1e01SJuraj Linkeš            SSHSessionDeadError: The session died while executing the command.
87840b1e01SJuraj Linkeš            SSHTimeoutError: The command execution timed out.
88840b1e01SJuraj Linkeš        """
89840b1e01SJuraj Linkeš        try:
90840b1e01SJuraj Linkeš            output = self.session.run(command, env=env, warn=True, hide=True, timeout=timeout)
91840b1e01SJuraj Linkeš
92840b1e01SJuraj Linkeš        except (UnexpectedExit, ThreadException) as e:
93840b1e01SJuraj Linkeš            self._logger.exception(e)
94840b1e01SJuraj Linkeš            raise SSHSessionDeadError(self.hostname) from e
95840b1e01SJuraj Linkeš
96840b1e01SJuraj Linkeš        except CommandTimedOut as e:
97840b1e01SJuraj Linkeš            self._logger.exception(e)
98840b1e01SJuraj Linkeš            raise SSHTimeoutError(command) from e
99840b1e01SJuraj Linkeš
100840b1e01SJuraj Linkeš        return CommandResult(self.name, command, output.stdout, output.stderr, output.return_code)
101840b1e01SJuraj Linkeš
1026e3cdef8SJuraj Linkeš    def is_alive(self) -> bool:
1036e3cdef8SJuraj Linkeš        """Overrides :meth:`~.remote_session.RemoteSession.is_alive`."""
1046e3cdef8SJuraj Linkeš        return self.session.is_connected
1056e3cdef8SJuraj Linkeš
106*441c5fbfSTomáš Ďurovec    def copy_from(self, source_file: str | PurePath, destination_dir: str | Path) -> None:
1076ef07151SJuraj Linkeš        """Overrides :meth:`~.remote_session.RemoteSession.copy_from`."""
108*441c5fbfSTomáš Ďurovec        self.session.get(str(source_file), str(destination_dir))
109840b1e01SJuraj Linkeš
110*441c5fbfSTomáš Ďurovec    def copy_to(self, source_file: str | Path, destination_dir: str | PurePath) -> None:
1116ef07151SJuraj Linkeš        """Overrides :meth:`~.remote_session.RemoteSession.copy_to`."""
112*441c5fbfSTomáš Ďurovec        self.session.put(str(source_file), str(destination_dir))
113840b1e01SJuraj Linkeš
1146e3cdef8SJuraj Linkeš    def close(self) -> None:
1156e3cdef8SJuraj Linkeš        """Overrides :meth:`~.remote_session.RemoteSession.close`."""
116840b1e01SJuraj Linkeš        self.session.close()
117