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