xref: /dpdk/dts/framework/testbed_model/sut_node.py (revision 2bf48044dca1892e571fd4964eecaacf6cb0c1c2)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2010-2014 Intel Corporation
3# Copyright(c) 2023 PANTHEON.tech s.r.o.
4
5import os
6import tarfile
7import time
8from pathlib import PurePath
9
10from framework.config import BuildTargetConfiguration, NodeConfiguration
11from framework.remote_session import CommandResult, OSSession
12from framework.settings import SETTINGS
13from framework.utils import EnvVarsDict, MesonArgs
14
15from .hw import LogicalCoreCount, LogicalCoreList, VirtualDevice
16from .node import Node
17
18
19class SutNode(Node):
20    """
21    A class for managing connections to the System under Test, providing
22    methods that retrieve the necessary information about the node (such as
23    CPU, memory and NIC details) and configuration capabilities.
24    Another key capability is building DPDK according to given build target.
25    """
26
27    _dpdk_prefix_list: list[str]
28    _dpdk_timestamp: str
29    _build_target_config: BuildTargetConfiguration | None
30    _env_vars: EnvVarsDict
31    _remote_tmp_dir: PurePath
32    __remote_dpdk_dir: PurePath | None
33    _dpdk_version: str | None
34    _app_compile_timeout: float
35    _dpdk_kill_session: OSSession | None
36
37    def __init__(self, node_config: NodeConfiguration):
38        super(SutNode, self).__init__(node_config)
39        self._dpdk_prefix_list = []
40        self._build_target_config = None
41        self._env_vars = EnvVarsDict()
42        self._remote_tmp_dir = self.main_session.get_remote_tmp_dir()
43        self.__remote_dpdk_dir = None
44        self._dpdk_version = None
45        self._app_compile_timeout = 90
46        self._dpdk_kill_session = None
47        self._dpdk_timestamp = (
48            f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}"
49        )
50
51    @property
52    def _remote_dpdk_dir(self) -> PurePath:
53        if self.__remote_dpdk_dir is None:
54            self.__remote_dpdk_dir = self._guess_dpdk_remote_dir()
55        return self.__remote_dpdk_dir
56
57    @_remote_dpdk_dir.setter
58    def _remote_dpdk_dir(self, value: PurePath) -> None:
59        self.__remote_dpdk_dir = value
60
61    @property
62    def remote_dpdk_build_dir(self) -> PurePath:
63        if self._build_target_config:
64            return self.main_session.join_remote_path(
65                self._remote_dpdk_dir, self._build_target_config.name
66            )
67        else:
68            return self.main_session.join_remote_path(self._remote_dpdk_dir, "build")
69
70    @property
71    def dpdk_version(self) -> str:
72        if self._dpdk_version is None:
73            self._dpdk_version = self.main_session.get_dpdk_version(
74                self._remote_dpdk_dir
75            )
76        return self._dpdk_version
77
78    def _guess_dpdk_remote_dir(self) -> PurePath:
79        return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir)
80
81    def _set_up_build_target(
82        self, build_target_config: BuildTargetConfiguration
83    ) -> None:
84        """
85        Setup DPDK on the SUT node.
86        """
87        self._configure_build_target(build_target_config)
88        self._copy_dpdk_tarball()
89        self._build_dpdk()
90
91    def _configure_build_target(
92        self, build_target_config: BuildTargetConfiguration
93    ) -> None:
94        """
95        Populate common environment variables and set build target config.
96        """
97        self._env_vars = EnvVarsDict()
98        self._build_target_config = build_target_config
99        self._env_vars.update(
100            self.main_session.get_dpdk_build_env_vars(build_target_config.arch)
101        )
102        self._env_vars["CC"] = build_target_config.compiler.name
103        if build_target_config.compiler_wrapper:
104            self._env_vars["CC"] = (
105                f"'{build_target_config.compiler_wrapper} "
106                f"{build_target_config.compiler.name}'"
107            )
108
109    @Node.skip_setup
110    def _copy_dpdk_tarball(self) -> None:
111        """
112        Copy to and extract DPDK tarball on the SUT node.
113        """
114        self._logger.info("Copying DPDK tarball to SUT.")
115        self.main_session.copy_file(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir)
116
117        # construct remote tarball path
118        # the basename is the same on local host and on remote Node
119        remote_tarball_path = self.main_session.join_remote_path(
120            self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path)
121        )
122
123        # construct remote path after extracting
124        with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar:
125            dpdk_top_dir = dpdk_tar.getnames()[0]
126        self._remote_dpdk_dir = self.main_session.join_remote_path(
127            self._remote_tmp_dir, dpdk_top_dir
128        )
129
130        self._logger.info(
131            f"Extracting DPDK tarball on SUT: "
132            f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'."
133        )
134        # clean remote path where we're extracting
135        self.main_session.remove_remote_dir(self._remote_dpdk_dir)
136
137        # then extract to remote path
138        self.main_session.extract_remote_tarball(
139            remote_tarball_path, self._remote_dpdk_dir
140        )
141
142    @Node.skip_setup
143    def _build_dpdk(self) -> None:
144        """
145        Build DPDK. Uses the already configured target. Assumes that the tarball has
146        already been copied to and extracted on the SUT node.
147        """
148        self.main_session.build_dpdk(
149            self._env_vars,
150            MesonArgs(default_library="static", enable_kmods=True, libdir="lib"),
151            self._remote_dpdk_dir,
152            self.remote_dpdk_build_dir,
153        )
154
155    def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath:
156        """
157        Build one or all DPDK apps. Requires DPDK to be already built on the SUT node.
158        When app_name is 'all', build all example apps.
159        When app_name is any other string, tries to build that example app.
160        Return the directory path of the built app. If building all apps, return
161        the path to the examples directory (where all apps reside).
162        The meson_dpdk_args are keyword arguments
163        found in meson_option.txt in root DPDK directory. Do not use -D with them,
164        for example: enable_kmods=True.
165        """
166        self.main_session.build_dpdk(
167            self._env_vars,
168            MesonArgs(examples=app_name, **meson_dpdk_args),  # type: ignore [arg-type]
169            # ^^ https://github.com/python/mypy/issues/11583
170            self._remote_dpdk_dir,
171            self.remote_dpdk_build_dir,
172            rebuild=True,
173            timeout=self._app_compile_timeout,
174        )
175
176        if app_name == "all":
177            return self.main_session.join_remote_path(
178                self.remote_dpdk_build_dir, "examples"
179            )
180        return self.main_session.join_remote_path(
181            self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}"
182        )
183
184    def kill_cleanup_dpdk_apps(self) -> None:
185        """
186        Kill all dpdk applications on the SUT. Cleanup hugepages.
187        """
188        if self._dpdk_kill_session and self._dpdk_kill_session.is_alive():
189            # we can use the session if it exists and responds
190            self._dpdk_kill_session.kill_cleanup_dpdk_apps(self._dpdk_prefix_list)
191        else:
192            # otherwise, we need to (re)create it
193            self._dpdk_kill_session = self.create_session("dpdk_kill")
194        self._dpdk_prefix_list = []
195
196    def create_eal_parameters(
197        self,
198        lcore_filter_specifier: LogicalCoreCount | LogicalCoreList = LogicalCoreCount(),
199        ascending_cores: bool = True,
200        prefix: str = "dpdk",
201        append_prefix_timestamp: bool = True,
202        no_pci: bool = False,
203        vdevs: list[VirtualDevice] = None,
204        other_eal_param: str = "",
205    ) -> "EalParameters":
206        """
207        Generate eal parameters character string;
208        :param lcore_filter_specifier: a number of lcores/cores/sockets to use
209                        or a list of lcore ids to use.
210                        The default will select one lcore for each of two cores
211                        on one socket, in ascending order of core ids.
212        :param ascending_cores: True, use cores with the lowest numerical id first
213                        and continue in ascending order. If False, start with the
214                        highest id and continue in descending order. This ordering
215                        affects which sockets to consider first as well.
216        :param prefix: set file prefix string, eg:
217                        prefix='vf'
218        :param append_prefix_timestamp: if True, will append a timestamp to
219                        DPDK file prefix.
220        :param no_pci: switch of disable PCI bus eg:
221                        no_pci=True
222        :param vdevs: virtual device list, eg:
223                        vdevs=[
224                            VirtualDevice('net_ring0'),
225                            VirtualDevice('net_ring1')
226                        ]
227        :param other_eal_param: user defined DPDK eal parameters, eg:
228                        other_eal_param='--single-file-segments'
229        :return: eal param string, eg:
230                '-c 0xf -a 0000:88:00.0 --file-prefix=dpdk_1112_20190809143420';
231        """
232
233        lcore_list = LogicalCoreList(
234            self.filter_lcores(lcore_filter_specifier, ascending_cores)
235        )
236
237        if append_prefix_timestamp:
238            prefix = f"{prefix}_{self._dpdk_timestamp}"
239        prefix = self.main_session.get_dpdk_file_prefix(prefix)
240        if prefix:
241            self._dpdk_prefix_list.append(prefix)
242
243        if vdevs is None:
244            vdevs = []
245
246        return EalParameters(
247            lcore_list=lcore_list,
248            memory_channels=self.config.memory_channels,
249            prefix=prefix,
250            no_pci=no_pci,
251            vdevs=vdevs,
252            other_eal_param=other_eal_param,
253        )
254
255    def run_dpdk_app(
256        self, app_path: PurePath, eal_args: "EalParameters", timeout: float = 30
257    ) -> CommandResult:
258        """
259        Run DPDK application on the remote node.
260        """
261        return self.main_session.send_command(
262            f"{app_path} {eal_args}", timeout, verify=True
263        )
264
265
266class EalParameters(object):
267    def __init__(
268        self,
269        lcore_list: LogicalCoreList,
270        memory_channels: int,
271        prefix: str,
272        no_pci: bool,
273        vdevs: list[VirtualDevice],
274        other_eal_param: str,
275    ):
276        """
277        Generate eal parameters character string;
278        :param lcore_list: the list of logical cores to use.
279        :param memory_channels: the number of memory channels to use.
280        :param prefix: set file prefix string, eg:
281                        prefix='vf'
282        :param no_pci: switch of disable PCI bus eg:
283                        no_pci=True
284        :param vdevs: virtual device list, eg:
285                        vdevs=[
286                            VirtualDevice('net_ring0'),
287                            VirtualDevice('net_ring1')
288                        ]
289        :param other_eal_param: user defined DPDK eal parameters, eg:
290                        other_eal_param='--single-file-segments'
291        """
292        self._lcore_list = f"-l {lcore_list}"
293        self._memory_channels = f"-n {memory_channels}"
294        self._prefix = prefix
295        if prefix:
296            self._prefix = f"--file-prefix={prefix}"
297        self._no_pci = "--no-pci" if no_pci else ""
298        self._vdevs = " ".join(f"--vdev {vdev}" for vdev in vdevs)
299        self._other_eal_param = other_eal_param
300
301    def __str__(self) -> str:
302        return (
303            f"{self._lcore_list} "
304            f"{self._memory_channels} "
305            f"{self._prefix} "
306            f"{self._no_pci} "
307            f"{self._vdevs} "
308            f"{self._other_eal_param}"
309        )
310