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