1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2023 PANTHEON.tech s.r.o. 3# Copyright(c) 2023 University of New Hampshire 4 5"""OS-aware remote session. 6 7DPDK supports multiple different operating systems, meaning it can run on these different operating 8systems. This module defines the common API that OS-unaware layers use and translates the API into 9OS-aware calls/utility usage. 10 11Note: 12 Running commands with administrative privileges requires OS awareness. This is the only layer 13 that's aware of OS differences, so this is where non-privileged command get converted 14 to privileged commands. 15 16Example: 17 A user wishes to remove a directory on a remote :class:`~.sut_node.SutNode`. 18 The :class:`~.sut_node.SutNode` object isn't aware what OS the node is running - it delegates 19 the OS translation logic to :attr:`~.node.Node.main_session`. The SUT node calls 20 :meth:`~OSSession.remove_remote_dir` with a generic, OS-unaware path and 21 the :attr:`~.node.Node.main_session` translates that to ``rm -rf`` if the node's OS is Linux 22 and other commands for other OSs. It also translates the path to match the underlying OS. 23""" 24 25from abc import ABC, abstractmethod 26from collections.abc import Iterable 27from ipaddress import IPv4Interface, IPv6Interface 28from pathlib import PurePath 29from typing import Type, TypeVar, Union 30 31from framework.config import Architecture, NodeConfiguration, NodeInfo 32from framework.logger import DTSLOG 33from framework.remote_session import ( 34 CommandResult, 35 InteractiveRemoteSession, 36 InteractiveShell, 37 RemoteSession, 38 create_interactive_session, 39 create_remote_session, 40) 41from framework.settings import SETTINGS 42from framework.utils import MesonArgs 43 44from .cpu import LogicalCore 45from .port import Port 46 47InteractiveShellType = TypeVar("InteractiveShellType", bound=InteractiveShell) 48 49 50class OSSession(ABC): 51 """OS-unaware to OS-aware translation API definition. 52 53 The OSSession classes create a remote session to a DTS node and implement OS specific 54 behavior. There a few control methods implemented by the base class, the rest need 55 to be implemented by subclasses. 56 57 Attributes: 58 name: The name of the session. 59 remote_session: The remote session maintaining the connection to the node. 60 interactive_session: The interactive remote session maintaining the connection to the node. 61 """ 62 63 _config: NodeConfiguration 64 name: str 65 _logger: DTSLOG 66 remote_session: RemoteSession 67 interactive_session: InteractiveRemoteSession 68 69 def __init__( 70 self, 71 node_config: NodeConfiguration, 72 name: str, 73 logger: DTSLOG, 74 ): 75 """Initialize the OS-aware session. 76 77 Connect to the node right away and also create an interactive remote session. 78 79 Args: 80 node_config: The test run configuration of the node to connect to. 81 name: The name of the session. 82 logger: The logger instance this session will use. 83 """ 84 self._config = node_config 85 self.name = name 86 self._logger = logger 87 self.remote_session = create_remote_session(node_config, name, logger) 88 self.interactive_session = create_interactive_session(node_config, logger) 89 90 def close(self, force: bool = False) -> None: 91 """Close the underlying remote session. 92 93 Args: 94 force: Force the closure of the connection. 95 """ 96 self.remote_session.close(force) 97 98 def is_alive(self) -> bool: 99 """Check whether the underlying remote session is still responding.""" 100 return self.remote_session.is_alive() 101 102 def send_command( 103 self, 104 command: str, 105 timeout: float = SETTINGS.timeout, 106 privileged: bool = False, 107 verify: bool = False, 108 env: dict | None = None, 109 ) -> CommandResult: 110 """An all-purpose API for OS-agnostic commands. 111 112 This can be used for an execution of a portable command that's executed the same way 113 on all operating systems, such as Python. 114 115 The :option:`--timeout` command line argument and the :envvar:`DTS_TIMEOUT` 116 environment variable configure the timeout of command execution. 117 118 Args: 119 command: The command to execute. 120 timeout: Wait at most this long in seconds for `command` execution to complete. 121 privileged: Whether to run the command with administrative privileges. 122 verify: If :data:`True`, will check the exit code of the command. 123 env: A dictionary with environment variables to be used with the command execution. 124 125 Raises: 126 RemoteCommandExecutionError: If verify is :data:`True` and the command failed. 127 """ 128 if privileged: 129 command = self._get_privileged_command(command) 130 131 return self.remote_session.send_command(command, timeout, verify, env) 132 133 def create_interactive_shell( 134 self, 135 shell_cls: Type[InteractiveShellType], 136 timeout: float, 137 privileged: bool, 138 app_args: str, 139 ) -> InteractiveShellType: 140 """Factory for interactive session handlers. 141 142 Instantiate `shell_cls` according to the remote OS specifics. 143 144 Args: 145 shell_cls: The class of the shell. 146 timeout: Timeout for reading output from the SSH channel. If you are 147 reading from the buffer and don't receive any data within the timeout 148 it will throw an error. 149 privileged: Whether to run the shell with administrative privileges. 150 app_args: The arguments to be passed to the application. 151 152 Returns: 153 An instance of the desired interactive application shell. 154 """ 155 return shell_cls( 156 self.interactive_session.session, 157 self._logger, 158 self._get_privileged_command if privileged else None, 159 app_args, 160 timeout, 161 ) 162 163 @staticmethod 164 @abstractmethod 165 def _get_privileged_command(command: str) -> str: 166 """Modify the command so that it executes with administrative privileges. 167 168 Args: 169 command: The command to modify. 170 171 Returns: 172 The modified command that executes with administrative privileges. 173 """ 174 175 @abstractmethod 176 def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePath: 177 """Try to find DPDK directory in `remote_dir`. 178 179 The directory is the one which is created after the extraction of the tarball. The files 180 are usually extracted into a directory starting with ``dpdk-``. 181 182 Returns: 183 The absolute path of the DPDK remote directory, empty path if not found. 184 """ 185 186 @abstractmethod 187 def get_remote_tmp_dir(self) -> PurePath: 188 """Get the path of the temporary directory of the remote OS. 189 190 Returns: 191 The absolute path of the temporary directory. 192 """ 193 194 @abstractmethod 195 def get_dpdk_build_env_vars(self, arch: Architecture) -> dict: 196 """Create extra environment variables needed for the target architecture. 197 198 Different architectures may require different configuration, such as setting 32-bit CFLAGS. 199 200 Returns: 201 A dictionary with keys as environment variables. 202 """ 203 204 @abstractmethod 205 def join_remote_path(self, *args: str | PurePath) -> PurePath: 206 """Join path parts using the path separator that fits the remote OS. 207 208 Args: 209 args: Any number of paths to join. 210 211 Returns: 212 The resulting joined path. 213 """ 214 215 @abstractmethod 216 def copy_from( 217 self, 218 source_file: str | PurePath, 219 destination_file: str | PurePath, 220 ) -> None: 221 """Copy a file from the remote node to the local filesystem. 222 223 Copy `source_file` from the remote node associated with this remote 224 session to `destination_file` on the local filesystem. 225 226 Args: 227 source_file: the file on the remote node. 228 destination_file: a file or directory path on the local filesystem. 229 """ 230 231 @abstractmethod 232 def copy_to( 233 self, 234 source_file: str | PurePath, 235 destination_file: str | PurePath, 236 ) -> None: 237 """Copy a file from local filesystem to the remote node. 238 239 Copy `source_file` from local filesystem to `destination_file` 240 on the remote node associated with this remote session. 241 242 Args: 243 source_file: the file on the local filesystem. 244 destination_file: a file or directory path on the remote node. 245 """ 246 247 @abstractmethod 248 def remove_remote_dir( 249 self, 250 remote_dir_path: str | PurePath, 251 recursive: bool = True, 252 force: bool = True, 253 ) -> None: 254 """Remove remote directory, by default remove recursively and forcefully. 255 256 Args: 257 remote_dir_path: The path of the directory to remove. 258 recursive: If :data:`True`, also remove all contents inside the directory. 259 force: If :data:`True`, ignore all warnings and try to remove at all costs. 260 """ 261 262 @abstractmethod 263 def extract_remote_tarball( 264 self, 265 remote_tarball_path: str | PurePath, 266 expected_dir: str | PurePath | None = None, 267 ) -> None: 268 """Extract remote tarball in its remote directory. 269 270 Args: 271 remote_tarball_path: The path of the tarball on the remote node. 272 expected_dir: If non-empty, check whether `expected_dir` exists after extracting 273 the archive. 274 """ 275 276 @abstractmethod 277 def build_dpdk( 278 self, 279 env_vars: dict, 280 meson_args: MesonArgs, 281 remote_dpdk_dir: str | PurePath, 282 remote_dpdk_build_dir: str | PurePath, 283 rebuild: bool = False, 284 timeout: float = SETTINGS.compile_timeout, 285 ) -> None: 286 """Build DPDK on the remote node. 287 288 An extracted DPDK tarball must be present on the node. The build consists of two steps:: 289 290 meson setup <meson args> remote_dpdk_dir remote_dpdk_build_dir 291 ninja -C remote_dpdk_build_dir 292 293 The :option:`--compile-timeout` command line argument and the :envvar:`DTS_COMPILE_TIMEOUT` 294 environment variable configure the timeout of DPDK build. 295 296 Args: 297 env_vars: Use these environment variables when building DPDK. 298 meson_args: Use these meson arguments when building DPDK. 299 remote_dpdk_dir: The directory on the remote node where DPDK will be built. 300 remote_dpdk_build_dir: The target build directory on the remote node. 301 rebuild: If :data:`True`, do a subsequent build with ``meson configure`` instead 302 of ``meson setup``. 303 timeout: Wait at most this long in seconds for the build execution to complete. 304 """ 305 306 @abstractmethod 307 def get_dpdk_version(self, version_path: str | PurePath) -> str: 308 """Inspect the DPDK version on the remote node. 309 310 Args: 311 version_path: The path to the VERSION file containing the DPDK version. 312 313 Returns: 314 The DPDK version. 315 """ 316 317 @abstractmethod 318 def get_remote_cpus(self, use_first_core: bool) -> list[LogicalCore]: 319 r"""Get the list of :class:`~.cpu.LogicalCore`\s on the remote node. 320 321 Args: 322 use_first_core: If :data:`False`, the first physical core won't be used. 323 324 Returns: 325 The logical cores present on the node. 326 """ 327 328 @abstractmethod 329 def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None: 330 """Kill and cleanup all DPDK apps. 331 332 Args: 333 dpdk_prefix_list: Kill all apps identified by `dpdk_prefix_list`. 334 If `dpdk_prefix_list` is empty, attempt to find running DPDK apps to kill and clean. 335 """ 336 337 @abstractmethod 338 def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str: 339 """Make OS-specific modification to the DPDK file prefix. 340 341 Args: 342 dpdk_prefix: The OS-unaware file prefix. 343 344 Returns: 345 The OS-specific file prefix. 346 """ 347 348 @abstractmethod 349 def setup_hugepages(self, hugepage_count: int, force_first_numa: bool) -> None: 350 """Configure hugepages on the node. 351 352 Get the node's Hugepage Size, configure the specified count of hugepages 353 if needed and mount the hugepages if needed. 354 355 Args: 356 hugepage_count: Configure this many hugepages. 357 force_first_numa: If :data:`True`, configure hugepages just on the first numa node. 358 """ 359 360 @abstractmethod 361 def get_compiler_version(self, compiler_name: str) -> str: 362 """Get installed version of compiler used for DPDK. 363 364 Args: 365 compiler_name: The name of the compiler executable. 366 367 Returns: 368 The compiler's version. 369 """ 370 371 @abstractmethod 372 def get_node_info(self) -> NodeInfo: 373 """Collect additional information about the node. 374 375 Returns: 376 Node information. 377 """ 378 379 @abstractmethod 380 def update_ports(self, ports: list[Port]) -> None: 381 """Get additional information about ports from the operating system and update them. 382 383 The additional information is: 384 385 * Logical name (e.g. ``enp7s0``) if applicable, 386 * Mac address. 387 388 Args: 389 ports: The ports to update. 390 """ 391 392 @abstractmethod 393 def configure_port_state(self, port: Port, enable: bool) -> None: 394 """Enable/disable `port` in the operating system. 395 396 Args: 397 port: The port to configure. 398 enable: If :data:`True`, enable the port, otherwise shut it down. 399 """ 400 401 @abstractmethod 402 def configure_port_ip_address( 403 self, 404 address: Union[IPv4Interface, IPv6Interface], 405 port: Port, 406 delete: bool, 407 ) -> None: 408 """Configure an IP address on `port` in the operating system. 409 410 Args: 411 address: The address to configure. 412 port: The port to configure. 413 delete: If :data:`True`, remove the IP address, otherwise configure it. 414 """ 415 416 @abstractmethod 417 def configure_ipv4_forwarding(self, enable: bool) -> None: 418 """Enable IPv4 forwarding in the operating system. 419 420 Args: 421 enable: If :data:`True`, enable the forwarding, otherwise disable it. 422 """ 423