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