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