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 _path_to_devbind_script: PurePath | None 93 94 def __init__(self, node_config: SutNodeConfiguration): 95 super(SutNode, self).__init__(node_config) 96 self._dpdk_prefix_list = [] 97 self._build_target_config = None 98 self._env_vars = {} 99 self._remote_tmp_dir = self.main_session.get_remote_tmp_dir() 100 self.__remote_dpdk_dir = None 101 self._app_compile_timeout = 90 102 self._dpdk_kill_session = None 103 self._dpdk_timestamp = ( 104 f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}" 105 ) 106 self._dpdk_version = None 107 self._node_info = None 108 self._compiler_version = None 109 self._path_to_devbind_script = None 110 self._logger.info(f"Created node: {self.name}") 111 112 @property 113 def _remote_dpdk_dir(self) -> PurePath: 114 if self.__remote_dpdk_dir is None: 115 self.__remote_dpdk_dir = self._guess_dpdk_remote_dir() 116 return self.__remote_dpdk_dir 117 118 @_remote_dpdk_dir.setter 119 def _remote_dpdk_dir(self, value: PurePath) -> None: 120 self.__remote_dpdk_dir = value 121 122 @property 123 def remote_dpdk_build_dir(self) -> PurePath: 124 if self._build_target_config: 125 return self.main_session.join_remote_path( 126 self._remote_dpdk_dir, self._build_target_config.name 127 ) 128 else: 129 return self.main_session.join_remote_path(self._remote_dpdk_dir, "build") 130 131 @property 132 def dpdk_version(self) -> str: 133 if self._dpdk_version is None: 134 self._dpdk_version = self.main_session.get_dpdk_version( 135 self._remote_dpdk_dir 136 ) 137 return self._dpdk_version 138 139 @property 140 def node_info(self) -> NodeInfo: 141 if self._node_info is None: 142 self._node_info = self.main_session.get_node_info() 143 return self._node_info 144 145 @property 146 def compiler_version(self) -> str: 147 if self._compiler_version is None: 148 if self._build_target_config is not None: 149 self._compiler_version = self.main_session.get_compiler_version( 150 self._build_target_config.compiler.name 151 ) 152 else: 153 self._logger.warning( 154 "Failed to get compiler version because" 155 "_build_target_config is None." 156 ) 157 return "" 158 return self._compiler_version 159 160 @property 161 def path_to_devbind_script(self) -> PurePath: 162 if self._path_to_devbind_script is None: 163 self._path_to_devbind_script = self.main_session.join_remote_path( 164 self._remote_dpdk_dir, "usertools", "dpdk-devbind.py" 165 ) 166 return self._path_to_devbind_script 167 168 def get_build_target_info(self) -> BuildTargetInfo: 169 return BuildTargetInfo( 170 dpdk_version=self.dpdk_version, compiler_version=self.compiler_version 171 ) 172 173 def _guess_dpdk_remote_dir(self) -> PurePath: 174 return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) 175 176 def _set_up_build_target( 177 self, build_target_config: BuildTargetConfiguration 178 ) -> None: 179 """ 180 Setup DPDK on the SUT node. 181 """ 182 # we want to ensure that dpdk_version and compiler_version is reset for new 183 # build targets 184 self._dpdk_version = None 185 self._compiler_version = None 186 self._configure_build_target(build_target_config) 187 self._copy_dpdk_tarball() 188 self._build_dpdk() 189 self.bind_ports_to_driver() 190 191 def _tear_down_build_target(self) -> None: 192 """ 193 This method exists to be optionally overwritten by derived classes and 194 is not decorated so that the derived class doesn't have to use the decorator. 195 """ 196 self.bind_ports_to_driver(for_dpdk=False) 197 198 def _configure_build_target( 199 self, build_target_config: BuildTargetConfiguration 200 ) -> None: 201 """ 202 Populate common environment variables and set build target config. 203 """ 204 self._env_vars = {} 205 self._build_target_config = build_target_config 206 self._env_vars.update( 207 self.main_session.get_dpdk_build_env_vars(build_target_config.arch) 208 ) 209 self._env_vars["CC"] = build_target_config.compiler.name 210 if build_target_config.compiler_wrapper: 211 self._env_vars["CC"] = ( 212 f"'{build_target_config.compiler_wrapper} " 213 f"{build_target_config.compiler.name}'" 214 ) 215 216 @Node.skip_setup 217 def _copy_dpdk_tarball(self) -> None: 218 """ 219 Copy to and extract DPDK tarball on the SUT node. 220 """ 221 self._logger.info("Copying DPDK tarball to SUT.") 222 self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir) 223 224 # construct remote tarball path 225 # the basename is the same on local host and on remote Node 226 remote_tarball_path = self.main_session.join_remote_path( 227 self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path) 228 ) 229 230 # construct remote path after extracting 231 with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar: 232 dpdk_top_dir = dpdk_tar.getnames()[0] 233 self._remote_dpdk_dir = self.main_session.join_remote_path( 234 self._remote_tmp_dir, dpdk_top_dir 235 ) 236 237 self._logger.info( 238 f"Extracting DPDK tarball on SUT: " 239 f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'." 240 ) 241 # clean remote path where we're extracting 242 self.main_session.remove_remote_dir(self._remote_dpdk_dir) 243 244 # then extract to remote path 245 self.main_session.extract_remote_tarball( 246 remote_tarball_path, self._remote_dpdk_dir 247 ) 248 249 @Node.skip_setup 250 def _build_dpdk(self) -> None: 251 """ 252 Build DPDK. Uses the already configured target. Assumes that the tarball has 253 already been copied to and extracted on the SUT node. 254 """ 255 self.main_session.build_dpdk( 256 self._env_vars, 257 MesonArgs(default_library="static", enable_kmods=True, libdir="lib"), 258 self._remote_dpdk_dir, 259 self.remote_dpdk_build_dir, 260 ) 261 262 def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath: 263 """ 264 Build one or all DPDK apps. Requires DPDK to be already built on the SUT node. 265 When app_name is 'all', build all example apps. 266 When app_name is any other string, tries to build that example app. 267 Return the directory path of the built app. If building all apps, return 268 the path to the examples directory (where all apps reside). 269 The meson_dpdk_args are keyword arguments 270 found in meson_option.txt in root DPDK directory. Do not use -D with them, 271 for example: enable_kmods=True. 272 """ 273 self.main_session.build_dpdk( 274 self._env_vars, 275 MesonArgs(examples=app_name, **meson_dpdk_args), # type: ignore [arg-type] 276 # ^^ https://github.com/python/mypy/issues/11583 277 self._remote_dpdk_dir, 278 self.remote_dpdk_build_dir, 279 rebuild=True, 280 timeout=self._app_compile_timeout, 281 ) 282 283 if app_name == "all": 284 return self.main_session.join_remote_path( 285 self.remote_dpdk_build_dir, "examples" 286 ) 287 return self.main_session.join_remote_path( 288 self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}" 289 ) 290 291 def kill_cleanup_dpdk_apps(self) -> None: 292 """ 293 Kill all dpdk applications on the SUT. Cleanup hugepages. 294 """ 295 if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(): 296 # we can use the session if it exists and responds 297 self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list) 298 else: 299 # otherwise, we need to (re)create it 300 self._dpdk_kill_session = self.create_session("dpdk_kill") 301 self._dpdk_prefix_list = [] 302 303 def create_eal_parameters( 304 self, 305 lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(), 306 ascending_cores: bool = True, 307 prefix: str = "dpdk", 308 append_prefix_timestamp: bool = True, 309 no_pci: bool = False, 310 vdevs: list[VirtualDevice] = None, 311 other_eal_param: str = "", 312 ) -> "EalParameters": 313 """ 314 Generate eal parameters character string; 315 :param lcore_filter_specifier: a number of lcores/cores/sockets to use 316 or a list of lcore ids to use. 317 The default will select one lcore for each of two cores 318 on one socket, in ascending order of core ids. 319 :param ascending_cores: True, use cores with the lowest numerical id first 320 and continue in ascending order. If False, start with the 321 highest id and continue in descending order. This ordering 322 affects which sockets to consider first as well. 323 :param prefix: set file prefix string, eg: 324 prefix='vf' 325 :param append_prefix_timestamp: if True, will append a timestamp to 326 DPDK file prefix. 327 :param no_pci: switch of disable PCI bus eg: 328 no_pci=True 329 :param vdevs: virtual device list, eg: 330 vdevs=[ 331 VirtualDevice('net_ring0'), 332 VirtualDevice('net_ring1') 333 ] 334 :param other_eal_param: user defined DPDK eal parameters, eg: 335 other_eal_param='--single-file-segments' 336 :return: eal param string, eg: 337 '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420'; 338 """ 339 340 lcore_list = LogicalCoreList( 341 self.filter_lcores(lcore_filter_specifier, ascending_cores) 342 ) 343 344 if append_prefix_timestamp: 345 prefix = f"{prefix}_{self._dpdk_timestamp}" 346 prefix = self.main_session.get_dpdk_file_prefix(prefix) 347 if prefix: 348 self._dpdk_prefix_list.append(prefix) 349 350 if vdevs is None: 351 vdevs = [] 352 353 return EalParameters( 354 lcore_list=lcore_list, 355 memory_channels=self.config.memory_channels, 356 prefix=prefix, 357 no_pci=no_pci, 358 vdevs=vdevs, 359 other_eal_param=other_eal_param, 360 ) 361 362 def run_dpdk_app( 363 self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30 364 ) -> CommandResult: 365 """ 366 Run DPDK application on the remote node. 367 """ 368 return self.main_session.send_command( 369 f"{app_path} {eal_args}", timeout, privileged=True, verify=True 370 ) 371 372 def configure_ipv4_forwarding(self, enable: bool) -> None: 373 self.main_session.configure_ipv4_forwarding(enable) 374 375 def create_interactive_shell( 376 self, 377 shell_cls: Type[InteractiveShellType], 378 timeout: float = SETTINGS.timeout, 379 privileged: bool = False, 380 eal_parameters: EalParameters | str | None = None, 381 ) -> InteractiveShellType: 382 """Factory method for creating a handler for an interactive session. 383 384 Instantiate shell_cls according to the remote OS specifics. 385 386 Args: 387 shell_cls: The class of the shell. 388 timeout: Timeout for reading output from the SSH channel. If you are 389 reading from the buffer and don't receive any data within the timeout 390 it will throw an error. 391 privileged: Whether to run the shell with administrative privileges. 392 eal_parameters: List of EAL parameters to use to launch the app. If this 393 isn't provided or an empty string is passed, it will default to calling 394 create_eal_parameters(). 395 Returns: 396 Instance of the desired interactive application. 397 """ 398 if not eal_parameters: 399 eal_parameters = self.create_eal_parameters() 400 401 # We need to append the build directory for DPDK apps 402 if shell_cls.dpdk_app: 403 shell_cls.path = self.main_session.join_remote_path( 404 self.remote_dpdk_build_dir, shell_cls.path 405 ) 406 407 return super().create_interactive_shell( 408 shell_cls, timeout, privileged, str(eal_parameters) 409 ) 410 411 def bind_ports_to_driver(self, for_dpdk: bool = True) -> None: 412 """Bind all ports on the SUT to a driver. 413 414 Args: 415 for_dpdk: Boolean that, when True, binds ports to os_driver_for_dpdk 416 or, when False, binds to os_driver. Defaults to True. 417 """ 418 for port in self.ports: 419 driver = port.os_driver_for_dpdk if for_dpdk else port.os_driver 420 self.main_session.send_command( 421 f"{self.path_to_devbind_script} -b {driver} --force {port.pci}", 422 privileged=True, 423 verify=True, 424 ) 425