xref: /dpdk/dts/framework/remote_session/remote_session.py (revision 441c5fbf939b55f635d42ad9b7dcc3741e1c2a7c)
1840b1e01SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause
2840b1e01SJuraj Linkeš# Copyright(c) 2010-2014 Intel Corporation
3840b1e01SJuraj Linkeš# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
4840b1e01SJuraj Linkeš# Copyright(c) 2022-2023 University of New Hampshire
53b8dd3b9SLuca Vizzarro# Copyright(c) 2024 Arm Limited
6840b1e01SJuraj Linkeš
76ef07151SJuraj Linkeš"""Base remote session.
86ef07151SJuraj Linkeš
96ef07151SJuraj LinkešThis module contains the abstract base class for remote sessions and defines
106ef07151SJuraj Linkešthe structure of the result of a command execution.
116ef07151SJuraj Linkeš"""
126ef07151SJuraj Linkeš
13840b1e01SJuraj Linkešfrom abc import ABC, abstractmethod
14c9fb6822SJuraj Linkešfrom dataclasses import InitVar, dataclass, field
15*441c5fbfSTomáš Ďurovecfrom pathlib import Path, PurePath
16840b1e01SJuraj Linkeš
17840b1e01SJuraj Linkešfrom framework.config import NodeConfiguration
18840b1e01SJuraj Linkešfrom framework.exception import RemoteCommandExecutionError
1904f5a5a6SJuraj Linkešfrom framework.logger import DTSLogger
20840b1e01SJuraj Linkešfrom framework.settings import SETTINGS
21840b1e01SJuraj Linkeš
22840b1e01SJuraj Linkeš
23c9fb6822SJuraj Linkeš@dataclass(slots=True, frozen=True)
24840b1e01SJuraj Linkešclass CommandResult:
256ef07151SJuraj Linkeš    """The result of remote execution of a command.
266ef07151SJuraj Linkeš
276ef07151SJuraj Linkeš    Attributes:
286ef07151SJuraj Linkeš        name: The name of the session that executed the command.
296ef07151SJuraj Linkeš        command: The executed command.
306ef07151SJuraj Linkeš        stdout: The standard output the command produced.
316ef07151SJuraj Linkeš        stderr: The standard error output the command produced.
326ef07151SJuraj Linkeš        return_code: The return code the command exited with.
33840b1e01SJuraj Linkeš    """
34840b1e01SJuraj Linkeš
35840b1e01SJuraj Linkeš    name: str
36840b1e01SJuraj Linkeš    command: str
37c9fb6822SJuraj Linkeš    init_stdout: InitVar[str]
38c9fb6822SJuraj Linkeš    init_stderr: InitVar[str]
39840b1e01SJuraj Linkeš    return_code: int
40c9fb6822SJuraj Linkeš    stdout: str = field(init=False)
41c9fb6822SJuraj Linkeš    stderr: str = field(init=False)
42c9fb6822SJuraj Linkeš
43c9fb6822SJuraj Linkeš    def __post_init__(self, init_stdout: str, init_stderr: str) -> None:
44c9fb6822SJuraj Linkeš        """Strip the whitespaces from stdout and stderr.
45c9fb6822SJuraj Linkeš
46c9fb6822SJuraj Linkeš        The generated __init__ method uses object.__setattr__() when the dataclass is frozen,
47c9fb6822SJuraj Linkeš        so that's what we use here as well.
48c9fb6822SJuraj Linkeš
49c9fb6822SJuraj Linkeš        In order to get access to dataclass fields in the __post_init__ method,
50c9fb6822SJuraj Linkeš        we have to type them as InitVars. These InitVars are included in the __init__ method's
51c9fb6822SJuraj Linkeš        signature, so we have to exclude the actual stdout and stderr fields
52c9fb6822SJuraj Linkeš        from the __init__ method's signature, so that we have the proper number of arguments.
53c9fb6822SJuraj Linkeš        """
54c9fb6822SJuraj Linkeš        object.__setattr__(self, "stdout", init_stdout.strip())
55c9fb6822SJuraj Linkeš        object.__setattr__(self, "stderr", init_stderr.strip())
56840b1e01SJuraj Linkeš
57840b1e01SJuraj Linkeš    def __str__(self) -> str:
586ef07151SJuraj Linkeš        """Format the command outputs."""
59840b1e01SJuraj Linkeš        return (
60840b1e01SJuraj Linkeš            f"stdout: '{self.stdout}'\n"
61840b1e01SJuraj Linkeš            f"stderr: '{self.stderr}'\n"
62840b1e01SJuraj Linkeš            f"return_code: '{self.return_code}'"
63840b1e01SJuraj Linkeš        )
64840b1e01SJuraj Linkeš
65840b1e01SJuraj Linkeš
66840b1e01SJuraj Linkešclass RemoteSession(ABC):
676ef07151SJuraj Linkeš    """Non-interactive remote session.
686ef07151SJuraj Linkeš
696ef07151SJuraj Linkeš    The abstract methods must be implemented in order to connect to a remote host (node)
706ef07151SJuraj Linkeš    and maintain a remote session.
716ef07151SJuraj Linkeš    The subclasses must use (or implement) some underlying transport protocol (e.g. SSH)
726ef07151SJuraj Linkeš    to implement the methods. On top of that, it provides some basic services common to all
736ef07151SJuraj Linkeš    subclasses, such as keeping history and logging what's being executed on the remote node.
746ef07151SJuraj Linkeš
756ef07151SJuraj Linkeš    Attributes:
766ef07151SJuraj Linkeš        name: The name of the session.
776ef07151SJuraj Linkeš        hostname: The node's hostname. Could be an IP (possibly with port, separated by a colon)
786ef07151SJuraj Linkeš            or a domain name.
796ef07151SJuraj Linkeš        ip: The IP address of the node or a domain name, whichever was used in `hostname`.
806ef07151SJuraj Linkeš        port: The port of the node, if given in `hostname`.
816ef07151SJuraj Linkeš        username: The username used in the connection.
826ef07151SJuraj Linkeš        password: The password used in the connection. Most frequently empty,
836ef07151SJuraj Linkeš            as the use of passwords is discouraged.
846ef07151SJuraj Linkeš        history: The executed commands during this session.
85840b1e01SJuraj Linkeš    """
86840b1e01SJuraj Linkeš
87840b1e01SJuraj Linkeš    name: str
88840b1e01SJuraj Linkeš    hostname: str
89840b1e01SJuraj Linkeš    ip: str
90840b1e01SJuraj Linkeš    port: int | None
91840b1e01SJuraj Linkeš    username: str
92840b1e01SJuraj Linkeš    password: str
93840b1e01SJuraj Linkeš    history: list[CommandResult]
9404f5a5a6SJuraj Linkeš    _logger: DTSLogger
95840b1e01SJuraj Linkeš    _node_config: NodeConfiguration
96840b1e01SJuraj Linkeš
97840b1e01SJuraj Linkeš    def __init__(
98840b1e01SJuraj Linkeš        self,
99840b1e01SJuraj Linkeš        node_config: NodeConfiguration,
100840b1e01SJuraj Linkeš        session_name: str,
10104f5a5a6SJuraj Linkeš        logger: DTSLogger,
102840b1e01SJuraj Linkeš    ):
1036ef07151SJuraj Linkeš        """Connect to the node during initialization.
1046ef07151SJuraj Linkeš
1056ef07151SJuraj Linkeš        Args:
1066ef07151SJuraj Linkeš            node_config: The test run configuration of the node to connect to.
1076ef07151SJuraj Linkeš            session_name: The name of the session.
1086ef07151SJuraj Linkeš            logger: The logger instance this session will use.
1096ef07151SJuraj Linkeš
1106ef07151SJuraj Linkeš        Raises:
1116ef07151SJuraj Linkeš            SSHConnectionError: If the connection to the node was not successful.
1126ef07151SJuraj Linkeš        """
113840b1e01SJuraj Linkeš        self._node_config = node_config
114840b1e01SJuraj Linkeš
115840b1e01SJuraj Linkeš        self.name = session_name
116840b1e01SJuraj Linkeš        self.hostname = node_config.hostname
117840b1e01SJuraj Linkeš        self.ip = self.hostname
118840b1e01SJuraj Linkeš        self.port = None
119840b1e01SJuraj Linkeš        if ":" in self.hostname:
120840b1e01SJuraj Linkeš            self.ip, port = self.hostname.split(":")
121840b1e01SJuraj Linkeš            self.port = int(port)
122840b1e01SJuraj Linkeš        self.username = node_config.user
123840b1e01SJuraj Linkeš        self.password = node_config.password or ""
124840b1e01SJuraj Linkeš        self.history = []
125840b1e01SJuraj Linkeš
126840b1e01SJuraj Linkeš        self._logger = logger
127840b1e01SJuraj Linkeš        self._logger.info(f"Connecting to {self.username}@{self.hostname}.")
128840b1e01SJuraj Linkeš        self._connect()
129840b1e01SJuraj Linkeš        self._logger.info(f"Connection to {self.username}@{self.hostname} successful.")
130840b1e01SJuraj Linkeš
131840b1e01SJuraj Linkeš    @abstractmethod
132840b1e01SJuraj Linkeš    def _connect(self) -> None:
1336ef07151SJuraj Linkeš        """Create a connection to the node.
1346ef07151SJuraj Linkeš
1356ef07151SJuraj Linkeš        The implementation must assign the established session to self.session.
1366ef07151SJuraj Linkeš
1376ef07151SJuraj Linkeš        The implementation must except all exceptions and convert them to an SSHConnectionError.
1386ef07151SJuraj Linkeš
1396ef07151SJuraj Linkeš        The implementation may optionally implement retry attempts.
140840b1e01SJuraj Linkeš        """
141840b1e01SJuraj Linkeš
142840b1e01SJuraj Linkeš    def send_command(
143840b1e01SJuraj Linkeš        self,
144840b1e01SJuraj Linkeš        command: str,
145840b1e01SJuraj Linkeš        timeout: float = SETTINGS.timeout,
146840b1e01SJuraj Linkeš        verify: bool = False,
147840b1e01SJuraj Linkeš        env: dict | None = None,
148840b1e01SJuraj Linkeš    ) -> CommandResult:
1496ef07151SJuraj Linkeš        """Send `command` to the connected node.
1506ef07151SJuraj Linkeš
1516ef07151SJuraj Linkeš        The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT`
1526ef07151SJuraj Linkeš        environment variable configure the timeout of command execution.
1536ef07151SJuraj Linkeš
1546ef07151SJuraj Linkeš        Args:
1556ef07151SJuraj Linkeš            command: The command to execute.
1566ef07151SJuraj Linkeš            timeout: Wait at most this long in seconds for `command` execution to complete.
1576ef07151SJuraj Linkeš            verify: If :data:`True`, will check the exit code of `command`.
1586ef07151SJuraj Linkeš            env: A dictionary with environment variables to be used with `command` execution.
1596ef07151SJuraj Linkeš
1606ef07151SJuraj Linkeš        Raises:
1616ef07151SJuraj Linkeš            SSHSessionDeadError: If the session isn't alive when sending `command`.
1626ef07151SJuraj Linkeš            SSHTimeoutError: If `command` execution timed out.
1636ef07151SJuraj Linkeš            RemoteCommandExecutionError: If verify is :data:`True` and `command` execution failed.
1646ef07151SJuraj Linkeš
1656ef07151SJuraj Linkeš        Returns:
1666ef07151SJuraj Linkeš            The output of the command along with the return code.
167840b1e01SJuraj Linkeš        """
168840b1e01SJuraj Linkeš        self._logger.info(f"Sending: '{command}'" + (f" with env vars: '{env}'" if env else ""))
169840b1e01SJuraj Linkeš        result = self._send_command(command, timeout, env)
170840b1e01SJuraj Linkeš        if verify and result.return_code:
171840b1e01SJuraj Linkeš            self._logger.debug(
172840b1e01SJuraj Linkeš                f"Command '{command}' failed with return code '{result.return_code}'"
173840b1e01SJuraj Linkeš            )
174840b1e01SJuraj Linkeš            self._logger.debug(f"stdout: '{result.stdout}'")
175840b1e01SJuraj Linkeš            self._logger.debug(f"stderr: '{result.stderr}'")
1763b8dd3b9SLuca Vizzarro            raise RemoteCommandExecutionError(command, result.stderr, result.return_code)
177840b1e01SJuraj Linkeš        self._logger.debug(f"Received from '{command}':\n{result}")
178840b1e01SJuraj Linkeš        self.history.append(result)
179840b1e01SJuraj Linkeš        return result
180840b1e01SJuraj Linkeš
181840b1e01SJuraj Linkeš    @abstractmethod
182840b1e01SJuraj Linkeš    def _send_command(self, command: str, timeout: float, env: dict | None) -> CommandResult:
1836ef07151SJuraj Linkeš        """Send a command to the connected node.
1846ef07151SJuraj Linkeš
1856ef07151SJuraj Linkeš        The implementation must execute the command remotely with `env` environment variables
1866ef07151SJuraj Linkeš        and return the result.
1876ef07151SJuraj Linkeš
1886ef07151SJuraj Linkeš        The implementation must except all exceptions and raise:
1896ef07151SJuraj Linkeš
1906ef07151SJuraj Linkeš            * SSHSessionDeadError if the session is not alive,
1916ef07151SJuraj Linkeš            * SSHTimeoutError if the command execution times out.
192840b1e01SJuraj Linkeš        """
193840b1e01SJuraj Linkeš
194840b1e01SJuraj Linkeš    @abstractmethod
195840b1e01SJuraj Linkeš    def is_alive(self) -> bool:
1966ef07151SJuraj Linkeš        """Check whether the remote session is still responding."""
197840b1e01SJuraj Linkeš
198840b1e01SJuraj Linkeš    @abstractmethod
199*441c5fbfSTomáš Ďurovec    def copy_from(self, source_file: str | PurePath, destination_dir: str | Path) -> None:
200840b1e01SJuraj Linkeš        """Copy a file from the remote Node to the local filesystem.
201840b1e01SJuraj Linkeš
2026ef07151SJuraj Linkeš        Copy `source_file` from the remote Node associated with this remote session
203*441c5fbfSTomáš Ďurovec        to `destination_dir` on the local filesystem.
204840b1e01SJuraj Linkeš
205840b1e01SJuraj Linkeš        Args:
2066ef07151SJuraj Linkeš            source_file: The file on the remote Node.
207*441c5fbfSTomáš Ďurovec            destination_dir: The directory path on the local filesystem where the `source_file`
208*441c5fbfSTomáš Ďurovec                will be saved.
209840b1e01SJuraj Linkeš        """
210840b1e01SJuraj Linkeš
211840b1e01SJuraj Linkeš    @abstractmethod
212*441c5fbfSTomáš Ďurovec    def copy_to(self, source_file: str | Path, destination_dir: str | PurePath) -> None:
213840b1e01SJuraj Linkeš        """Copy a file from local filesystem to the remote Node.
214840b1e01SJuraj Linkeš
215*441c5fbfSTomáš Ďurovec        Copy `source_file` from local filesystem to `destination_dir` on the remote Node
2166ef07151SJuraj Linkeš        associated with this remote session.
217840b1e01SJuraj Linkeš
218840b1e01SJuraj Linkeš        Args:
2196ef07151SJuraj Linkeš            source_file: The file on the local filesystem.
220*441c5fbfSTomáš Ďurovec            destination_dir: The directory path on the remote Node where the `source_file`
221*441c5fbfSTomáš Ďurovec                will be saved.
222840b1e01SJuraj Linkeš        """
2236e3cdef8SJuraj Linkeš
2246e3cdef8SJuraj Linkeš    @abstractmethod
2256e3cdef8SJuraj Linkeš    def close(self) -> None:
2266e3cdef8SJuraj Linkeš        """Close the remote session and free all used resources."""
227