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