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