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