xref: /dpdk/dts/framework/testbed_model/posix_session.py (revision e9fd1ebf981f361844aea9ec94e17f4bda5e1479)
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright(c) 2023 PANTHEON.tech s.r.o.
3# Copyright(c) 2023 University of New Hampshire
4
5"""POSIX compliant OS translator.
6
7Translates OS-unaware calls into POSIX compliant calls/utilities. POSIX is a set of standards
8for portability between Unix operating systems which not all Linux distributions
9(or the tools most frequently bundled with said distributions) adhere to. Most of Linux
10distributions are mostly compliant though.
11This intermediate module implements the common parts of mostly POSIX compliant distributions.
12"""
13
14import re
15from collections.abc import Iterable
16from pathlib import PurePath, PurePosixPath
17
18from framework.config import Architecture, NodeInfo
19from framework.exception import DPDKBuildError, RemoteCommandExecutionError
20from framework.settings import SETTINGS
21from framework.utils import MesonArgs
22
23from .os_session import OSSession
24
25
26class PosixSession(OSSession):
27    """An intermediary class implementing the POSIX standard."""
28
29    @staticmethod
30    def combine_short_options(**opts: bool) -> str:
31        """Combine shell options into one argument.
32
33        These are options such as ``-x``, ``-v``, ``-f`` which are combined into ``-xvf``.
34
35        Args:
36            opts: The keys are option names (usually one letter) and the bool values indicate
37                whether to include the option in the resulting argument.
38
39        Returns:
40            The options combined into one argument.
41        """
42        ret_opts = ""
43        for opt, include in opts.items():
44            if include:
45                ret_opts = f"{ret_opts}{opt}"
46
47        if ret_opts:
48            ret_opts = f" -{ret_opts}"
49
50        return ret_opts
51
52    def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePosixPath:
53        """Overrides :meth:`~.os_session.OSSession.guess_dpdk_remote_dir`."""
54        remote_guess = self.join_remote_path(remote_dir, "dpdk-*")
55        result = self.send_command(f"ls -d {remote_guess} | tail -1")
56        return PurePosixPath(result.stdout)
57
58    def get_remote_tmp_dir(self) -> PurePosixPath:
59        """Overrides :meth:`~.os_session.OSSession.get_remote_tmp_dir`."""
60        return PurePosixPath("/tmp")
61
62    def get_dpdk_build_env_vars(self, arch: Architecture) -> dict:
63        """Overrides :meth:`~.os_session.OSSession.get_dpdk_build_env_vars`.
64
65        Supported architecture: ``i686``.
66        """
67        env_vars = {}
68        if arch == Architecture.i686:
69            # find the pkg-config path and store it in PKG_CONFIG_LIBDIR
70            out = self.send_command("find /usr -type d -name pkgconfig")
71            pkg_path = ""
72            res_path = out.stdout.split("\r\n")
73            for cur_path in res_path:
74                if "i386" in cur_path:
75                    pkg_path = cur_path
76                    break
77            assert pkg_path != "", "i386 pkg-config path not found"
78
79            env_vars["CFLAGS"] = "-m32"
80            env_vars["PKG_CONFIG_LIBDIR"] = pkg_path
81
82        return env_vars
83
84    def join_remote_path(self, *args: str | PurePath) -> PurePosixPath:
85        """Overrides :meth:`~.os_session.OSSession.join_remote_path`."""
86        return PurePosixPath(*args)
87
88    def copy_from(
89        self,
90        source_file: str | PurePath,
91        destination_file: str | PurePath,
92    ) -> None:
93        """Overrides :meth:`~.os_session.OSSession.copy_from`."""
94        self.remote_session.copy_from(source_file, destination_file)
95
96    def copy_to(
97        self,
98        source_file: str | PurePath,
99        destination_file: str | PurePath,
100    ) -> None:
101        """Overrides :meth:`~.os_session.OSSession.copy_to`."""
102        self.remote_session.copy_to(source_file, destination_file)
103
104    def remove_remote_dir(
105        self,
106        remote_dir_path: str | PurePath,
107        recursive: bool = True,
108        force: bool = True,
109    ) -> None:
110        """Overrides :meth:`~.os_session.OSSession.remove_remote_dir`."""
111        opts = PosixSession.combine_short_options(r=recursive, f=force)
112        self.send_command(f"rm{opts} {remote_dir_path}")
113
114    def extract_remote_tarball(
115        self,
116        remote_tarball_path: str | PurePath,
117        expected_dir: str | PurePath | None = None,
118    ) -> None:
119        """Overrides :meth:`~.os_session.OSSession.extract_remote_tarball`."""
120        self.send_command(
121            f"tar xfm {remote_tarball_path} -C {PurePosixPath(remote_tarball_path).parent}",
122            60,
123        )
124        if expected_dir:
125            self.send_command(f"ls {expected_dir}", verify=True)
126
127    def build_dpdk(
128        self,
129        env_vars: dict,
130        meson_args: MesonArgs,
131        remote_dpdk_dir: str | PurePath,
132        remote_dpdk_build_dir: str | PurePath,
133        rebuild: bool = False,
134        timeout: float = SETTINGS.compile_timeout,
135    ) -> None:
136        """Overrides :meth:`~.os_session.OSSession.build_dpdk`."""
137        try:
138            if rebuild:
139                # reconfigure, then build
140                self._logger.info("Reconfiguring DPDK build.")
141                self.send_command(
142                    f"meson configure {meson_args} {remote_dpdk_build_dir}",
143                    timeout,
144                    verify=True,
145                    env=env_vars,
146                )
147            else:
148                # fresh build - remove target dir first, then build from scratch
149                self._logger.info("Configuring DPDK build from scratch.")
150                self.remove_remote_dir(remote_dpdk_build_dir)
151                self.send_command(
152                    f"meson setup {meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}",
153                    timeout,
154                    verify=True,
155                    env=env_vars,
156                )
157
158            self._logger.info("Building DPDK.")
159            self.send_command(
160                f"ninja -C {remote_dpdk_build_dir}", timeout, verify=True, env=env_vars
161            )
162        except RemoteCommandExecutionError as e:
163            raise DPDKBuildError(f"DPDK build failed when doing '{e.command}'.")
164
165    def get_dpdk_version(self, build_dir: str | PurePath) -> str:
166        """Overrides :meth:`~.os_session.OSSession.get_dpdk_version`."""
167        out = self.send_command(f"cat {self.join_remote_path(build_dir, 'VERSION')}", verify=True)
168        return out.stdout
169
170    def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None:
171        """Overrides :meth:`~.os_session.OSSession.kill_cleanup_dpdk_apps`."""
172        self._logger.info("Cleaning up DPDK apps.")
173        dpdk_runtime_dirs = self._get_dpdk_runtime_dirs(dpdk_prefix_list)
174        if dpdk_runtime_dirs:
175            # kill and cleanup only if DPDK is running
176            dpdk_pids = self._get_dpdk_pids(dpdk_runtime_dirs)
177            for dpdk_pid in dpdk_pids:
178                self.send_command(f"kill -9 {dpdk_pid}", 20)
179            self._check_dpdk_hugepages(dpdk_runtime_dirs)
180            self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs)
181
182    def _get_dpdk_runtime_dirs(self, dpdk_prefix_list: Iterable[str]) -> list[PurePosixPath]:
183        """Find runtime directories DPDK apps are currently using.
184
185        Args:
186              dpdk_prefix_list: The prefixes DPDK apps were started with.
187
188        Returns:
189            The paths of DPDK apps' runtime dirs.
190        """
191        prefix = PurePosixPath("/var", "run", "dpdk")
192        if not dpdk_prefix_list:
193            remote_prefixes = self._list_remote_dirs(prefix)
194            if not remote_prefixes:
195                dpdk_prefix_list = []
196            else:
197                dpdk_prefix_list = remote_prefixes
198
199        return [PurePosixPath(prefix, dpdk_prefix) for dpdk_prefix in dpdk_prefix_list]
200
201    def _list_remote_dirs(self, remote_path: str | PurePath) -> list[str] | None:
202        """Contents of remote_path.
203
204        Args:
205            remote_path: List the contents of this path.
206
207        Returns:
208            The contents of remote_path. If remote_path doesn't exist, return None.
209        """
210        out = self.send_command(f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'").stdout
211        if "No such file or directory" in out:
212            return None
213        else:
214            return out.splitlines()
215
216    def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> list[int]:
217        """Find PIDs of running DPDK apps.
218
219        Look at each "config" file found in dpdk_runtime_dirs and find the PIDs of processes
220        that opened those file.
221
222        Args:
223            dpdk_runtime_dirs: The paths of DPDK apps' runtime dirs.
224
225        Returns:
226            The PIDs of running DPDK apps.
227        """
228        pids = []
229        pid_regex = r"p(\d+)"
230        for dpdk_runtime_dir in dpdk_runtime_dirs:
231            dpdk_config_file = PurePosixPath(dpdk_runtime_dir, "config")
232            if self._remote_files_exists(dpdk_config_file):
233                out = self.send_command(f"lsof -Fp {dpdk_config_file}").stdout
234                if out and "No such file or directory" not in out:
235                    for out_line in out.splitlines():
236                        match = re.match(pid_regex, out_line)
237                        if match:
238                            pids.append(int(match.group(1)))
239        return pids
240
241    def _remote_files_exists(self, remote_path: PurePath) -> bool:
242        result = self.send_command(f"test -e {remote_path}")
243        return not result.return_code
244
245    def _check_dpdk_hugepages(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> None:
246        """Check there aren't any leftover hugepages.
247
248        If any hugepages are found, emit a warning. The hugepages are investigated in the
249        "hugepage_info" file of dpdk_runtime_dirs.
250
251        Args:
252            dpdk_runtime_dirs: The paths of DPDK apps' runtime dirs.
253        """
254        for dpdk_runtime_dir in dpdk_runtime_dirs:
255            hugepage_info = PurePosixPath(dpdk_runtime_dir, "hugepage_info")
256            if self._remote_files_exists(hugepage_info):
257                out = self.send_command(f"lsof -Fp {hugepage_info}").stdout
258                if out and "No such file or directory" not in out:
259                    self._logger.warning("Some DPDK processes did not free hugepages.")
260                    self._logger.warning("*******************************************")
261                    self._logger.warning(out)
262                    self._logger.warning("*******************************************")
263
264    def _remove_dpdk_runtime_dirs(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> None:
265        for dpdk_runtime_dir in dpdk_runtime_dirs:
266            self.remove_remote_dir(dpdk_runtime_dir)
267
268    def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str:
269        """Overrides :meth:`~.os_session.OSSession.get_dpdk_file_prefix`."""
270        return ""
271
272    def get_compiler_version(self, compiler_name: str) -> str:
273        """Overrides :meth:`~.os_session.OSSession.get_compiler_version`."""
274        match compiler_name:
275            case "gcc":
276                return self.send_command(
277                    f"{compiler_name} --version", SETTINGS.timeout
278                ).stdout.split("\n")[0]
279            case "clang":
280                return self.send_command(
281                    f"{compiler_name} --version", SETTINGS.timeout
282                ).stdout.split("\n")[0]
283            case "msvc":
284                return self.send_command("cl", SETTINGS.timeout).stdout
285            case "icc":
286                return self.send_command(f"{compiler_name} -V", SETTINGS.timeout).stdout
287            case _:
288                raise ValueError(f"Unknown compiler {compiler_name}")
289
290    def get_node_info(self) -> NodeInfo:
291        """Overrides :meth:`~.os_session.OSSession.get_node_info`."""
292        os_release_info = self.send_command(
293            "awk -F= '$1 ~ /^NAME$|^VERSION$/ {print $2}' /etc/os-release",
294            SETTINGS.timeout,
295        ).stdout.split("\n")
296        kernel_version = self.send_command("uname -r", SETTINGS.timeout).stdout
297        return NodeInfo(os_release_info[0].strip(), os_release_info[1].strip(), kernel_version)
298