xref: /dpdk/dts/framework/testbed_model/sut_node.py (revision da7e701151ea8b742d4c38ace3e4fefd1b4507fc)
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
93    def __init__(self, node_config: SutNodeConfiguration):
94        super(SutNode, self).__init__(node_config)
95        self._dpdk_prefix_list = []
96        self._build_target_config = None
97        self._env_vars = {}
98        self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
99        self.__remote_dpdk_dir = None
100        self._app_compile_timeout = 90
101        self._dpdk_kill_session = None
102        self._dpdk_timestamp = (
103            f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
104        )
105        self._dpdk_version = None
106        self._node_info = None
107        self._compiler_version = None
108        self._logger.info(f"Created node: {self.name}")
109
110    @property
111    def _remote_dpdk_dir(self) -> PurePath:
112        if self.__remote_dpdk_dir is None:
113            self.__remote_dpdk_dir = self._guess_dpdk_remote_dir()
114        return self.__remote_dpdk_dir
115
116    @_remote_dpdk_dir.setter
117    def _remote_dpdk_dir(self, value: PurePath) -> None:
118        self.__remote_dpdk_dir = value
119
120    @property
121    def remote_dpdk_build_dir(self) -> PurePath:
122        if self._build_target_config:
123            return self.main_session.join_remote_path(
124                self._remote_dpdk_dir, self._build_target_config.name
125            )
126        else:
127            return self.main_session.join_remote_path(self._remote_dpdk_dir, "build")
128
129    @property
130    def dpdk_version(self) -> str:
131        if self._dpdk_version is None:
132            self._dpdk_version = self.main_session.get_dpdk_version(
133                self._remote_dpdk_dir
134            )
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"
153                    "_build_target_config is None."
154                )
155                return ""
156        return self._compiler_version
157
158    def get_build_target_info(self) -> BuildTargetInfo:
159        return BuildTargetInfo(
160            dpdk_version=self.dpdk_version, compiler_version=self.compiler_version
161        )
162
163    def _guess_dpdk_remote_dir(self) -> PurePath:
164        return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir)
165
166    def _set_up_build_target(
167        self, build_target_config: BuildTargetConfiguration
168    ) -> None:
169        """
170        Setup DPDK on the SUT node.
171        """
172        # we want to ensure that dpdk_version and compiler_version is reset for new
173        # build targets
174        self._dpdk_version = None
175        self._compiler_version = None
176        self._configure_build_target(build_target_config)
177        self._copy_dpdk_tarball()
178        self._build_dpdk()
179
180    def _configure_build_target(
181        self, build_target_config: BuildTargetConfiguration
182    ) -> None:
183        """
184        Populate common environment variables and set build target config.
185        """
186        self._env_vars = {}
187        self._build_target_config = build_target_config
188        self._env_vars.update(
189            self.main_session.get_dpdk_build_env_vars(build_target_config.arch)
190        )
191        self._env_vars["CC"] = build_target_config.compiler.name
192        if build_target_config.compiler_wrapper:
193            self._env_vars["CC"] = (
194                f"'{build_target_config.compiler_wrapper} "
195                f"{build_target_config.compiler.name}'"
196            )
197
198    @Node.skip_setup
199    def _copy_dpdk_tarball(self) -> None:
200        """
201        Copy to and extract DPDK tarball on the SUT node.
202        """
203        self._logger.info("Copying DPDK tarball to SUT.")
204        self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir)
205
206        # construct remote tarball path
207        # the basename is the same on local host and on remote Node
208        remote_tarball_path = self.main_session.join_remote_path(
209            self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path)
210        )
211
212        # construct remote path after extracting
213        with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar:
214            dpdk_top_dir = dpdk_tar.getnames()[0]
215        self._remote_dpdk_dir = self.main_session.join_remote_path(
216            self._remote_tmp_dir, dpdk_top_dir
217        )
218
219        self._logger.info(
220            f"Extracting DPDK tarball on SUT: "
221            f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'."
222        )
223        # clean remote path where we're extracting
224        self.main_session.remove_remote_dir(self._remote_dpdk_dir)
225
226        # then extract to remote path
227        self.main_session.extract_remote_tarball(
228            remote_tarball_path, self._remote_dpdk_dir
229        )
230
231    @Node.skip_setup
232    def _build_dpdk(self) -> None:
233        """
234        Build DPDK. Uses the already configured target. Assumes that the tarball has
235        already been copied to and extracted on the SUT node.
236        """
237        self.main_session.build_dpdk(
238            self._env_vars,
239            MesonArgs(default_library="static", enable_kmods=True, libdir="lib"),
240            self._remote_dpdk_dir,
241            self.remote_dpdk_build_dir,
242        )
243
244    def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath:
245        """
246        Build one or all DPDK apps. Requires DPDK to be already built on the SUT node.
247        When app_name is 'all', build all example apps.
248        When app_name is any other string, tries to build that example app.
249        Return the directory path of the built app. If building all apps, return
250        the path to the examples directory (where all apps reside).
251        The meson_dpdk_args are keyword arguments
252        found in meson_option.txt in root DPDK directory. Do not use -D with them,
253        for example: enable_kmods=True.
254        """
255        self.main_session.build_dpdk(
256            self._env_vars,
257            MesonArgs(examples=app_name, **meson_dpdk_args),  # type: ignore [arg-type]
258            # ^^ https://github.com/python/mypy/issues/11583
259            self._remote_dpdk_dir,
260            self.remote_dpdk_build_dir,
261            rebuild=True,
262            timeout=self._app_compile_timeout,
263        )
264
265        if app_name == "all":
266            return self.main_session.join_remote_path(
267                self.remote_dpdk_build_dir, "examples"
268            )
269        return self.main_session.join_remote_path(
270            self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}"
271        )
272
273    def kill_cleanup_dpdk_apps(self) -> None:
274        """
275        Kill all dpdk applications on the SUT. Cleanup hugepages.
276        """
277        if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
278            # we can use the session if it exists and responds
279            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
280        else:
281            # otherwise, we need to (re)create it
282            self._dpdk_kill_session = self.create_session("dpdk_kill")
283        self._dpdk_prefix_list = []
284
285    def create_eal_parameters(
286        self,
287        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
288        ascending_cores: bool = True,
289        prefix: str = "dpdk",
290        append_prefix_timestamp: bool = True,
291        no_pci: bool = False,
292        vdevs: list[VirtualDevice] = None,
293        other_eal_param: str = "",
294    ) -> "EalParameters":
295        """
296        Generate eal parameters character string;
297        :param lcore_filter_specifier: a number of lcores/cores/sockets to use
298                        or a list of lcore ids to use.
299                        The default will select one lcore for each of two cores
300                        on one socket, in ascending order of core ids.
301        :param ascending_cores: True, use cores with the lowest numerical id first
302                        and continue in ascending order. If False, start with the
303                        highest id and continue in descending order. This ordering
304                        affects which sockets to consider first as well.
305        :param prefix: set file prefix string, eg:
306                        prefix='vf'
307        :param append_prefix_timestamp: if True, will append a timestamp to
308                        DPDK file prefix.
309        :param no_pci: switch of disable PCI bus eg:
310                        no_pci=True
311        :param vdevs: virtual device list, eg:
312                        vdevs=[
313                            VirtualDevice('net_ring0'),
314                            VirtualDevice('net_ring1')
315                        ]
316        :param other_eal_param: user defined DPDK eal parameters, eg:
317                        other_eal_param='--single-file-segments'
318        :return: eal param string, eg:
319                '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420';
320        """
321
322        lcore_list = LogicalCoreList(
323            self.filter_lcores(lcore_filter_specifier, ascending_cores)
324        )
325
326        if append_prefix_timestamp:
327            prefix = f"{prefix}_{self._dpdk_timestamp}"
328        prefix = self.main_session.get_dpdk_file_prefix(prefix)
329        if prefix:
330            self._dpdk_prefix_list.append(prefix)
331
332        if vdevs is None:
333            vdevs = []
334
335        return EalParameters(
336            lcore_list=lcore_list,
337            memory_channels=self.config.memory_channels,
338            prefix=prefix,
339            no_pci=no_pci,
340            vdevs=vdevs,
341            other_eal_param=other_eal_param,
342        )
343
344    def run_dpdk_app(
345        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
346    ) -> CommandResult:
347        """
348        Run DPDK application on the remote node.
349        """
350        return self.main_session.send_command(
351            f"{app_path} {eal_args}", timeout, privileged=True, verify=True
352        )
353
354    def configure_ipv4_forwarding(self, enable: bool) -> None:
355        self.main_session.configure_ipv4_forwarding(enable)
356
357    def create_interactive_shell(
358        self,
359        shell_cls: Type[InteractiveShellType],
360        timeout: float = SETTINGS.timeout,
361        privileged: bool = False,
362        eal_parameters: EalParameters | str | None = None,
363    ) -> InteractiveShellType:
364        """Factory method for creating a handler for an interactive session.
365
366        Instantiate shell_cls according to the remote OS specifics.
367
368        Args:
369            shell_cls: The class of the shell.
370            timeout: Timeout for reading output from the SSH channel. If you are
371                reading from the buffer and don't receive any data within the timeout
372                it will throw an error.
373            privileged: Whether to run the shell with administrative privileges.
374            eal_parameters: List of EAL parameters to use to launch the app. If this
375                isn't provided or an empty string is passed, it will default to calling
376                create_eal_parameters().
377        Returns:
378            Instance of the desired interactive application.
379        """
380        if not eal_parameters:
381            eal_parameters = self.create_eal_parameters()
382
383        # We need to append the build directory for DPDK apps
384        if shell_cls.dpdk_app:
385            shell_cls.path = self.main_session.join_remote_path(
386                self.remote_dpdk_build_dir, shell_cls.path
387            )
388
389        return super().create_interactive_shell(
390            shell_cls, timeout, privileged, str(eal_parameters)
391        )
392