1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2010-2014 Intel Corporation 3# Copyright(c) 2023 PANTHEON.tech s.r.o. 4# Copyright(c) 2023 University of New Hampshire 5 6import os 7import tarfile 8import time 9from pathlib import PurePath 10from typing import Type 11 12from framework.config import ( 13 BuildTargetConfiguration, 14 BuildTargetInfo, 15 NodeInfo, 16 SutNodeConfiguration, 17) 18from framework.remote_session import CommandResult, InteractiveShellType, OSSession 19from framework.settings import SETTINGS 20from framework.utils import MesonArgs 21 22from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice 23from .node import Node 24 25 26class EalParameters(object): 27 def __init__( 28 self, 29 lcore_list: LogicalCoreList, 30 memory_channels: int, 31 prefix: str, 32 no_pci: bool, 33 vdevs: list[VirtualDevice], 34 other_eal_param: str, 35 ): 36 """ 37 Generate eal parameters character string; 38 :param lcore_list: the list of logical cores to use. 39 :param memory_channels: the number of memory channels to use. 40 :param prefix: set file prefix string, eg: 41 prefix='vf' 42 :param no_pci: switch of disable PCI bus eg: 43 no_pci=True 44 :param vdevs: virtual device list, eg: 45 vdevs=[ 46 VirtualDevice('net_ring0'), 47 VirtualDevice('net_ring1') 48 ] 49 :param other_eal_param: user defined DPDK eal parameters, eg: 50 other_eal_param='--single-file-segments' 51 """ 52 self._lcore_list = f"-l {lcore_list}" 53 self._memory_channels = f"-n {memory_channels}" 54 self._prefix = prefix 55 if prefix: 56 self._prefix = f"--file-prefix={prefix}" 57 self._no_pci = "--no-pci" if no_pci else "" 58 self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs) 59 self._other_eal_param = other_eal_param 60 61 def __str__(self) -> str: 62 return ( 63 f"{self._lcore_list} " 64 f"{self._memory_channels} " 65 f"{self._prefix} " 66 f"{self._no_pci} " 67 f"{self._vdevs} " 68 f"{self._other_eal_param}" 69 ) 70 71 72class SutNode(Node): 73 """ 74 A class for managing connections to the System under Test, providing 75 methods that retrieve the necessary information about the node (such as 76 CPU, memory and NIC details) and configuration capabilities. 77 Another key capability is building DPDK according to given build target. 78 """ 79 80 config: SutNodeConfiguration 81 _dpdk_prefix_list: list[str] 82 _dpdk_timestamp: str 83 _build_target_config: BuildTargetConfiguration | None 84 _env_vars: dict 85 _remote_tmp_dir: PurePath 86 __remote_dpdk_dir: PurePath | None 87 _app_compile_timeout: float 88 _dpdk_kill_session: OSSession | None 89 _dpdk_version: str | None 90 _node_info: NodeInfo | None 91 _compiler_version: str | None 92 93 def __init__(self, node_config: SutNodeConfiguration): 94 super(SutNode, self).__init__(node_config) 95 self._dpdk_prefix_list = [] 96 self._build_target_config = None 97 self._env_vars = {} 98 self._remote_tmp_dir = self.main_session.get_remote_tmp_dir() 99 self.__remote_dpdk_dir = None 100 self._app_compile_timeout = 90 101 self._dpdk_kill_session = None 102 self._dpdk_timestamp = ( 103 f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}" 104 ) 105 self._dpdk_version = None 106 self._node_info = None 107 self._compiler_version = None 108 self._logger.info(f"Created node: {self.name}") 109 110 @property 111 def _remote_dpdk_dir(self) -> PurePath: 112 if self.__remote_dpdk_dir is None: 113 self.__remote_dpdk_dir = self._guess_dpdk_remote_dir() 114 return self.__remote_dpdk_dir 115 116 @_remote_dpdk_dir.setter 117 def _remote_dpdk_dir(self, value: PurePath) -> None: 118 self.__remote_dpdk_dir = value 119 120 @property 121 def remote_dpdk_build_dir(self) -> PurePath: 122 if self._build_target_config: 123 return self.main_session.join_remote_path( 124 self._remote_dpdk_dir, self._build_target_config.name 125 ) 126 else: 127 return self.main_session.join_remote_path(self._remote_dpdk_dir, "build") 128 129 @property 130 def dpdk_version(self) -> str: 131 if self._dpdk_version is None: 132 self._dpdk_version = self.main_session.get_dpdk_version( 133 self._remote_dpdk_dir 134 ) 135 return self._dpdk_version 136 137 @property 138 def node_info(self) -> NodeInfo: 139 if self._node_info is None: 140 self._node_info = self.main_session.get_node_info() 141 return self._node_info 142 143 @property 144 def compiler_version(self) -> str: 145 if self._compiler_version is None: 146 if self._build_target_config is not None: 147 self._compiler_version = self.main_session.get_compiler_version( 148 self._build_target_config.compiler.name 149 ) 150 else: 151 self._logger.warning( 152 "Failed to get compiler version because" 153 "_build_target_config is None." 154 ) 155 return "" 156 return self._compiler_version 157 158 def get_build_target_info(self) -> BuildTargetInfo: 159 return BuildTargetInfo( 160 dpdk_version=self.dpdk_version, compiler_version=self.compiler_version 161 ) 162 163 def _guess_dpdk_remote_dir(self) -> PurePath: 164 return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) 165 166 def _set_up_build_target( 167 self, build_target_config: BuildTargetConfiguration 168 ) -> None: 169 """ 170 Setup DPDK on the SUT node. 171 """ 172 # we want to ensure that dpdk_version and compiler_version is reset for new 173 # build targets 174 self._dpdk_version = None 175 self._compiler_version = None 176 self._configure_build_target(build_target_config) 177 self._copy_dpdk_tarball() 178 self._build_dpdk() 179 180 def _configure_build_target( 181 self, build_target_config: BuildTargetConfiguration 182 ) -> None: 183 """ 184 Populate common environment variables and set build target config. 185 """ 186 self._env_vars = {} 187 self._build_target_config = build_target_config 188 self._env_vars.update( 189 self.main_session.get_dpdk_build_env_vars(build_target_config.arch) 190 ) 191 self._env_vars["CC"] = build_target_config.compiler.name 192 if build_target_config.compiler_wrapper: 193 self._env_vars["CC"] = ( 194 f"'{build_target_config.compiler_wrapper} " 195 f"{build_target_config.compiler.name}'" 196 ) 197 198 @Node.skip_setup 199 def _copy_dpdk_tarball(self) -> None: 200 """ 201 Copy to and extract DPDK tarball on the SUT node. 202 """ 203 self._logger.info("Copying DPDK tarball to SUT.") 204 self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir) 205 206 # construct remote tarball path 207 # the basename is the same on local host and on remote Node 208 remote_tarball_path = self.main_session.join_remote_path( 209 self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path) 210 ) 211 212 # construct remote path after extracting 213 with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar: 214 dpdk_top_dir = dpdk_tar.getnames()[0] 215 self._remote_dpdk_dir = self.main_session.join_remote_path( 216 self._remote_tmp_dir, dpdk_top_dir 217 ) 218 219 self._logger.info( 220 f"Extracting DPDK tarball on SUT: " 221 f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'." 222 ) 223 # clean remote path where we're extracting 224 self.main_session.remove_remote_dir(self._remote_dpdk_dir) 225 226 # then extract to remote path 227 self.main_session.extract_remote_tarball( 228 remote_tarball_path, self._remote_dpdk_dir 229 ) 230 231 @Node.skip_setup 232 def _build_dpdk(self) -> None: 233 """ 234 Build DPDK. Uses the already configured target. Assumes that the tarball has 235 already been copied to and extracted on the SUT node. 236 """ 237 self.main_session.build_dpdk( 238 self._env_vars, 239 MesonArgs(default_library="static", enable_kmods=True, libdir="lib"), 240 self._remote_dpdk_dir, 241 self.remote_dpdk_build_dir, 242 ) 243 244 def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath: 245 """ 246 Build one or all DPDK apps. Requires DPDK to be already built on the SUT node. 247 When app_name is 'all', build all example apps. 248 When app_name is any other string, tries to build that example app. 249 Return the directory path of the built app. If building all apps, return 250 the path to the examples directory (where all apps reside). 251 The meson_dpdk_args are keyword arguments 252 found in meson_option.txt in root DPDK directory. Do not use -D with them, 253 for example: enable_kmods=True. 254 """ 255 self.main_session.build_dpdk( 256 self._env_vars, 257 MesonArgs(examples=app_name, **meson_dpdk_args), # type: ignore [arg-type] 258 # ^^ https://github.com/python/mypy/issues/11583 259 self._remote_dpdk_dir, 260 self.remote_dpdk_build_dir, 261 rebuild=True, 262 timeout=self._app_compile_timeout, 263 ) 264 265 if app_name == "all": 266 return self.main_session.join_remote_path( 267 self.remote_dpdk_build_dir, "examples" 268 ) 269 return self.main_session.join_remote_path( 270 self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}" 271 ) 272 273 def kill_cleanup_dpdk_apps(self) -> None: 274 """ 275 Kill all dpdk applications on the SUT. Cleanup hugepages. 276 """ 277 if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(): 278 # we can use the session if it exists and responds 279 self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list) 280 else: 281 # otherwise, we need to (re)create it 282 self._dpdk_kill_session = self.create_session("dpdk_kill") 283 self._dpdk_prefix_list = [] 284 285 def create_eal_parameters( 286 self, 287 lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(), 288 ascending_cores: bool = True, 289 prefix: str = "dpdk", 290 append_prefix_timestamp: bool = True, 291 no_pci: bool = False, 292 vdevs: list[VirtualDevice] = None, 293 other_eal_param: str = "", 294 ) -> "EalParameters": 295 """ 296 Generate eal parameters character string; 297 :param lcore_filter_specifier: a number of lcores/cores/sockets to use 298 or a list of lcore ids to use. 299 The default will select one lcore for each of two cores 300 on one socket, in ascending order of core ids. 301 :param ascending_cores: True, use cores with the lowest numerical id first 302 and continue in ascending order. If False, start with the 303 highest id and continue in descending order. This ordering 304 affects which sockets to consider first as well. 305 :param prefix: set file prefix string, eg: 306 prefix='vf' 307 :param append_prefix_timestamp: if True, will append a timestamp to 308 DPDK file prefix. 309 :param no_pci: switch of disable PCI bus eg: 310 no_pci=True 311 :param vdevs: virtual device list, eg: 312 vdevs=[ 313 VirtualDevice('net_ring0'), 314 VirtualDevice('net_ring1') 315 ] 316 :param other_eal_param: user defined DPDK eal parameters, eg: 317 other_eal_param='--single-file-segments' 318 :return: eal param string, eg: 319 '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420'; 320 """ 321 322 lcore_list = LogicalCoreList( 323 self.filter_lcores(lcore_filter_specifier, ascending_cores) 324 ) 325 326 if append_prefix_timestamp: 327 prefix = f"{prefix}_{self._dpdk_timestamp}" 328 prefix = self.main_session.get_dpdk_file_prefix(prefix) 329 if prefix: 330 self._dpdk_prefix_list.append(prefix) 331 332 if vdevs is None: 333 vdevs = [] 334 335 return EalParameters( 336 lcore_list=lcore_list, 337 memory_channels=self.config.memory_channels, 338 prefix=prefix, 339 no_pci=no_pci, 340 vdevs=vdevs, 341 other_eal_param=other_eal_param, 342 ) 343 344 def run_dpdk_app( 345 self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30 346 ) -> CommandResult: 347 """ 348 Run DPDK application on the remote node. 349 """ 350 return self.main_session.send_command( 351 f"{app_path} {eal_args}", timeout, privileged=True, verify=True 352 ) 353 354 def configure_ipv4_forwarding(self, enable: bool) -> None: 355 self.main_session.configure_ipv4_forwarding(enable) 356 357 def create_interactive_shell( 358 self, 359 shell_cls: Type[InteractiveShellType], 360 timeout: float = SETTINGS.timeout, 361 privileged: bool = False, 362 eal_parameters: EalParameters | str | None = None, 363 ) -> InteractiveShellType: 364 """Factory method for creating a handler for an interactive session. 365 366 Instantiate shell_cls according to the remote OS specifics. 367 368 Args: 369 shell_cls: The class of the shell. 370 timeout: Timeout for reading output from the SSH channel. If you are 371 reading from the buffer and don't receive any data within the timeout 372 it will throw an error. 373 privileged: Whether to run the shell with administrative privileges. 374 eal_parameters: List of EAL parameters to use to launch the app. If this 375 isn't provided or an empty string is passed, it will default to calling 376 create_eal_parameters(). 377 Returns: 378 Instance of the desired interactive application. 379 """ 380 if not eal_parameters: 381 eal_parameters = self.create_eal_parameters() 382 383 # We need to append the build directory for DPDK apps 384 if shell_cls.dpdk_app: 385 shell_cls.path = self.main_session.join_remote_path( 386 self.remote_dpdk_build_dir, shell_cls.path 387 ) 388 389 return super().create_interactive_shell( 390 shell_cls, timeout, privileged, str(eal_parameters) 391 ) 392