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# Copyright(c) 2024 Arm Limited 6 7"""System under test (DPDK + hardware) node. 8 9A system under test (SUT) is the combination of DPDK 10and the hardware we're testing with DPDK (NICs, crypto and other devices). 11An SUT node is where this SUT runs. 12""" 13 14 15import os 16import tarfile 17import time 18from pathlib import PurePath 19 20from framework.config import ( 21 BuildTargetConfiguration, 22 BuildTargetInfo, 23 NodeInfo, 24 SutNodeConfiguration, 25 TestRunConfiguration, 26) 27from framework.params.eal import EalParams 28from framework.remote_session.remote_session import CommandResult 29from framework.settings import SETTINGS 30from framework.utils import MesonArgs 31 32from .node import Node 33from .os_session import OSSession 34from .virtual_device import VirtualDevice 35 36 37class SutNode(Node): 38 """The system under test node. 39 40 The SUT node extends :class:`Node` with DPDK specific features: 41 42 * DPDK build, 43 * Gathering of DPDK build info, 44 * The running of DPDK apps, interactively or one-time execution, 45 * DPDK apps cleanup. 46 47 The :option:`--tarball` command line argument and the :envvar:`DTS_DPDK_TARBALL` 48 environment variable configure the path to the DPDK tarball 49 or the git commit ID, tag ID or tree ID to test. 50 51 Attributes: 52 config: The SUT node configuration. 53 virtual_devices: The virtual devices used on the node. 54 """ 55 56 config: SutNodeConfiguration 57 virtual_devices: list[VirtualDevice] 58 dpdk_prefix_list: list[str] 59 dpdk_timestamp: str 60 _build_target_config: BuildTargetConfiguration | None 61 _env_vars: dict 62 _remote_tmp_dir: PurePath 63 __remote_dpdk_dir: PurePath | None 64 _app_compile_timeout: float 65 _dpdk_kill_session: OSSession | None 66 _dpdk_version: str | None 67 _node_info: NodeInfo | None 68 _compiler_version: str | None 69 _path_to_devbind_script: PurePath | None 70 71 def __init__(self, node_config: SutNodeConfiguration): 72 """Extend the constructor with SUT node specifics. 73 74 Args: 75 node_config: The SUT node's test run configuration. 76 """ 77 super().__init__(node_config) 78 self.virtual_devices = [] 79 self.dpdk_prefix_list = [] 80 self._build_target_config = None 81 self._env_vars = {} 82 self._remote_tmp_dir = self.main_session.get_remote_tmp_dir() 83 self.__remote_dpdk_dir = None 84 self._app_compile_timeout = 90 85 self._dpdk_kill_session = None 86 self.dpdk_timestamp = ( 87 f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}" 88 ) 89 self._dpdk_version = None 90 self._node_info = None 91 self._compiler_version = None 92 self._path_to_devbind_script = None 93 self._logger.info(f"Created node: {self.name}") 94 95 @property 96 def _remote_dpdk_dir(self) -> PurePath: 97 """The remote DPDK dir. 98 99 This internal property should be set after extracting the DPDK tarball. If it's not set, 100 that implies the DPDK setup step has been skipped, in which case we can guess where 101 a previous build was located. 102 """ 103 if self.__remote_dpdk_dir is None: 104 self.__remote_dpdk_dir = self._guess_dpdk_remote_dir() 105 return self.__remote_dpdk_dir 106 107 @_remote_dpdk_dir.setter 108 def _remote_dpdk_dir(self, value: PurePath) -> None: 109 self.__remote_dpdk_dir = value 110 111 @property 112 def remote_dpdk_build_dir(self) -> PurePath: 113 """The remote DPDK build directory. 114 115 This is the directory where DPDK was built. 116 We assume it was built in a subdirectory of the extracted tarball. 117 """ 118 if self._build_target_config: 119 return self.main_session.join_remote_path( 120 self._remote_dpdk_dir, self._build_target_config.name 121 ) 122 else: 123 return self.main_session.join_remote_path(self._remote_dpdk_dir, "build") 124 125 @property 126 def dpdk_version(self) -> str: 127 """Last built DPDK version.""" 128 if self._dpdk_version is None: 129 self._dpdk_version = self.main_session.get_dpdk_version(self._remote_dpdk_dir) 130 return self._dpdk_version 131 132 @property 133 def node_info(self) -> NodeInfo: 134 """Additional node information.""" 135 if self._node_info is None: 136 self._node_info = self.main_session.get_node_info() 137 return self._node_info 138 139 @property 140 def compiler_version(self) -> str: 141 """The node's compiler version.""" 142 if self._compiler_version is None: 143 if self._build_target_config is not None: 144 self._compiler_version = self.main_session.get_compiler_version( 145 self._build_target_config.compiler.name 146 ) 147 else: 148 self._logger.warning( 149 "Failed to get compiler version because _build_target_config is None." 150 ) 151 return "" 152 return self._compiler_version 153 154 @property 155 def path_to_devbind_script(self) -> PurePath: 156 """The path to the dpdk-devbind.py script on the node.""" 157 if self._path_to_devbind_script is None: 158 self._path_to_devbind_script = self.main_session.join_remote_path( 159 self._remote_dpdk_dir, "usertools", "dpdk-devbind.py" 160 ) 161 return self._path_to_devbind_script 162 163 def get_build_target_info(self) -> BuildTargetInfo: 164 """Get additional build target information. 165 166 Returns: 167 The build target information, 168 """ 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_test_run(self, test_run_config: TestRunConfiguration) -> None: 177 """Extend the test run setup with vdev config. 178 179 Args: 180 test_run_config: A test run configuration according to which 181 the setup steps will be taken. 182 """ 183 super().set_up_test_run(test_run_config) 184 for vdev in test_run_config.vdevs: 185 self.virtual_devices.append(VirtualDevice(vdev)) 186 187 def tear_down_test_run(self) -> None: 188 """Extend the test run teardown with virtual device teardown.""" 189 super().tear_down_test_run() 190 self.virtual_devices = [] 191 192 def set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 193 """Set up DPDK the SUT node and bind ports. 194 195 DPDK setup includes setting all internals needed for the build, the copying of DPDK tarball 196 and then building DPDK. The drivers are bound to those that DPDK needs. 197 198 Args: 199 build_target_config: The build target test run configuration according to which 200 the setup steps will be taken. 201 """ 202 self._configure_build_target(build_target_config) 203 self._copy_dpdk_tarball() 204 self._build_dpdk() 205 self.bind_ports_to_driver() 206 207 def tear_down_build_target(self) -> None: 208 """Reset DPDK variables and bind port driver to the OS driver.""" 209 self._env_vars = {} 210 self._build_target_config = None 211 self.__remote_dpdk_dir = None 212 self._dpdk_version = None 213 self._compiler_version = None 214 self.bind_ports_to_driver(for_dpdk=False) 215 216 def _configure_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 217 """Populate common environment variables and set build target config.""" 218 self._env_vars = {} 219 self._build_target_config = build_target_config 220 self._env_vars.update(self.main_session.get_dpdk_build_env_vars(build_target_config.arch)) 221 self._env_vars["CC"] = build_target_config.compiler.name 222 if build_target_config.compiler_wrapper: 223 self._env_vars["CC"] = ( 224 f"'{build_target_config.compiler_wrapper} {build_target_config.compiler.name}'" 225 ) # fmt: skip 226 227 @Node.skip_setup 228 def _copy_dpdk_tarball(self) -> None: 229 """Copy to and extract DPDK tarball on the SUT node.""" 230 self._logger.info("Copying DPDK tarball to SUT.") 231 self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir) 232 233 # construct remote tarball path 234 # the basename is the same on local host and on remote Node 235 remote_tarball_path = self.main_session.join_remote_path( 236 self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path) 237 ) 238 239 # construct remote path after extracting 240 with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar: 241 dpdk_top_dir = dpdk_tar.getnames()[0] 242 self._remote_dpdk_dir = self.main_session.join_remote_path( 243 self._remote_tmp_dir, dpdk_top_dir 244 ) 245 246 self._logger.info( 247 f"Extracting DPDK tarball on SUT: " 248 f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'." 249 ) 250 # clean remote path where we're extracting 251 self.main_session.remove_remote_dir(self._remote_dpdk_dir) 252 253 # then extract to remote path 254 self.main_session.extract_remote_tarball(remote_tarball_path, self._remote_dpdk_dir) 255 256 @Node.skip_setup 257 def _build_dpdk(self) -> None: 258 """Build DPDK. 259 260 Uses the already configured target. Assumes that the tarball has 261 already been copied to and extracted on the SUT node. 262 """ 263 self.main_session.build_dpdk( 264 self._env_vars, 265 MesonArgs(default_library="static", enable_kmods=True, libdir="lib"), 266 self._remote_dpdk_dir, 267 self.remote_dpdk_build_dir, 268 ) 269 270 def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath: 271 """Build one or all DPDK apps. 272 273 Requires DPDK to be already built on the SUT node. 274 275 Args: 276 app_name: The name of the DPDK app to build. 277 When `app_name` is ``all``, build all example apps. 278 meson_dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory. 279 Do not use ``-D`` with them. 280 281 Returns: 282 The directory path of the built app. If building all apps, return 283 the path to the examples directory (where all apps reside). 284 """ 285 self.main_session.build_dpdk( 286 self._env_vars, 287 MesonArgs(examples=app_name, **meson_dpdk_args), # type: ignore [arg-type] 288 # ^^ https://github.com/python/mypy/issues/11583 289 self._remote_dpdk_dir, 290 self.remote_dpdk_build_dir, 291 rebuild=True, 292 timeout=self._app_compile_timeout, 293 ) 294 295 if app_name == "all": 296 return self.main_session.join_remote_path(self.remote_dpdk_build_dir, "examples") 297 return self.main_session.join_remote_path( 298 self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}" 299 ) 300 301 def kill_cleanup_dpdk_apps(self) -> None: 302 """Kill all dpdk applications on the SUT, then clean up hugepages.""" 303 if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(): 304 # we can use the session if it exists and responds 305 self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list) 306 else: 307 # otherwise, we need to (re)create it 308 self._dpdk_kill_session = self.create_session("dpdk_kill") 309 self.dpdk_prefix_list = [] 310 311 def run_dpdk_app( 312 self, app_path: PurePath, eal_params: EalParams, timeout: float = 30 313 ) -> CommandResult: 314 """Run DPDK application on the remote node. 315 316 The application is not run interactively - the command that starts the application 317 is executed and then the call waits for it to finish execution. 318 319 Args: 320 app_path: The remote path to the DPDK application. 321 eal_params: EAL parameters to run the DPDK application with. 322 timeout: Wait at most this long in seconds for `command` execution to complete. 323 324 Returns: 325 The result of the DPDK app execution. 326 """ 327 return self.main_session.send_command( 328 f"{app_path} {eal_params}", timeout, privileged=True, verify=True 329 ) 330 331 def configure_ipv4_forwarding(self, enable: bool) -> None: 332 """Enable/disable IPv4 forwarding on the node. 333 334 Args: 335 enable: If :data:`True`, enable the forwarding, otherwise disable it. 336 """ 337 self.main_session.configure_ipv4_forwarding(enable) 338 339 def bind_ports_to_driver(self, for_dpdk: bool = True) -> None: 340 """Bind all ports on the SUT to a driver. 341 342 Args: 343 for_dpdk: If :data:`True`, binds ports to os_driver_for_dpdk. 344 If :data:`False`, binds to os_driver. 345 """ 346 for port in self.ports: 347 driver = port.os_driver_for_dpdk if for_dpdk else port.os_driver 348 self.main_session.send_command( 349 f"{self.path_to_devbind_script} -b {driver} --force {port.pci}", 350 privileged=True, 351 verify=True, 352 ) 353