xref: /dpdk/dts/framework/testbed_model/sut_node.py (revision 7917b0d38e92e8b9ec5a870415b791420e10f11a)
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