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 Path, PurePath, PurePosixPath 17 18from framework.config import Architecture 19from framework.exception import DPDKBuildError, RemoteCommandExecutionError 20from framework.settings import SETTINGS 21from framework.utils import ( 22 MesonArgs, 23 TarCompressionFormat, 24 convert_to_list_of_string, 25 create_tarball, 26 extract_tarball, 27) 28 29from .os_session import OSSession, OSSessionInfo 30 31 32class PosixSession(OSSession): 33 """An intermediary class implementing the POSIX standard.""" 34 35 @staticmethod 36 def combine_short_options(**opts: bool) -> str: 37 """Combine shell options into one argument. 38 39 These are options such as ``-x``, ``-v``, ``-f`` which are combined into ``-xvf``. 40 41 Args: 42 opts: The keys are option names (usually one letter) and the bool values indicate 43 whether to include the option in the resulting argument. 44 45 Returns: 46 The options combined into one argument. 47 """ 48 ret_opts = "" 49 for opt, include in opts.items(): 50 if include: 51 ret_opts = f"{ret_opts}{opt}" 52 53 if ret_opts: 54 ret_opts = f" -{ret_opts}" 55 56 return ret_opts 57 58 def guess_dpdk_remote_dir(self, remote_dir: str | PurePath) -> PurePosixPath: 59 """Overrides :meth:`~.os_session.OSSession.guess_dpdk_remote_dir`.""" 60 remote_guess = self.join_remote_path(remote_dir, "dpdk-*") 61 result = self.send_command(f"ls -d {remote_guess} | tail -1") 62 return PurePosixPath(result.stdout) 63 64 def get_remote_tmp_dir(self) -> PurePosixPath: 65 """Overrides :meth:`~.os_session.OSSession.get_remote_tmp_dir`.""" 66 return PurePosixPath("/tmp") 67 68 def get_dpdk_build_env_vars(self, arch: Architecture) -> dict: 69 """Overrides :meth:`~.os_session.OSSession.get_dpdk_build_env_vars`. 70 71 Supported architecture: ``i686``. 72 """ 73 env_vars = {} 74 if arch == Architecture.i686: 75 # find the pkg-config path and store it in PKG_CONFIG_LIBDIR 76 out = self.send_command("find /usr -type d -name pkgconfig") 77 pkg_path = "" 78 res_path = out.stdout.split("\r\n") 79 for cur_path in res_path: 80 if "i386" in cur_path: 81 pkg_path = cur_path 82 break 83 assert pkg_path != "", "i386 pkg-config path not found" 84 85 env_vars["CFLAGS"] = "-m32" 86 env_vars["PKG_CONFIG_LIBDIR"] = pkg_path 87 88 return env_vars 89 90 def join_remote_path(self, *args: str | PurePath) -> PurePosixPath: 91 """Overrides :meth:`~.os_session.OSSession.join_remote_path`.""" 92 return PurePosixPath(*args) 93 94 def remote_path_exists(self, remote_path: str | PurePath) -> bool: 95 """Overrides :meth:`~.os_session.OSSession.remote_path_exists`.""" 96 result = self.send_command(f"test -e {remote_path}") 97 return not result.return_code 98 99 def copy_from(self, source_file: str | PurePath, destination_dir: str | Path) -> None: 100 """Overrides :meth:`~.os_session.OSSession.copy_from`.""" 101 self.remote_session.copy_from(source_file, destination_dir) 102 103 def copy_to(self, source_file: str | Path, destination_dir: str | PurePath) -> None: 104 """Overrides :meth:`~.os_session.OSSession.copy_to`.""" 105 self.remote_session.copy_to(source_file, destination_dir) 106 107 def copy_dir_from( 108 self, 109 source_dir: str | PurePath, 110 destination_dir: str | Path, 111 compress_format: TarCompressionFormat = TarCompressionFormat.none, 112 exclude: str | list[str] | None = None, 113 ) -> None: 114 """Overrides :meth:`~.os_session.OSSession.copy_dir_from`.""" 115 source_dir = PurePath(source_dir) 116 remote_tarball_path = self.create_remote_tarball(source_dir, compress_format, exclude) 117 118 self.copy_from(remote_tarball_path, destination_dir) 119 self.remove_remote_file(remote_tarball_path) 120 121 tarball_path = Path(destination_dir, f"{source_dir.name}.{compress_format.extension}") 122 extract_tarball(tarball_path) 123 tarball_path.unlink() 124 125 def copy_dir_to( 126 self, 127 source_dir: str | Path, 128 destination_dir: str | PurePath, 129 compress_format: TarCompressionFormat = TarCompressionFormat.none, 130 exclude: str | list[str] | None = None, 131 ) -> None: 132 """Overrides :meth:`~.os_session.OSSession.copy_dir_to`.""" 133 source_dir = Path(source_dir) 134 tarball_path = create_tarball(source_dir, compress_format, exclude=exclude) 135 self.copy_to(tarball_path, destination_dir) 136 tarball_path.unlink() 137 138 remote_tar_path = self.join_remote_path( 139 destination_dir, f"{source_dir.name}.{compress_format.extension}" 140 ) 141 self.extract_remote_tarball(remote_tar_path) 142 self.remove_remote_file(remote_tar_path) 143 144 def remove_remote_file(self, remote_file_path: str | PurePath, force: bool = True) -> None: 145 """Overrides :meth:`~.os_session.OSSession.remove_remote_dir`.""" 146 opts = PosixSession.combine_short_options(f=force) 147 self.send_command(f"rm{opts} {remote_file_path}") 148 149 def remove_remote_dir( 150 self, 151 remote_dir_path: str | PurePath, 152 recursive: bool = True, 153 force: bool = True, 154 ) -> None: 155 """Overrides :meth:`~.os_session.OSSession.remove_remote_dir`.""" 156 opts = PosixSession.combine_short_options(r=recursive, f=force) 157 self.send_command(f"rm{opts} {remote_dir_path}") 158 159 def create_remote_tarball( 160 self, 161 remote_dir_path: str | PurePath, 162 compress_format: TarCompressionFormat = TarCompressionFormat.none, 163 exclude: str | list[str] | None = None, 164 ) -> PurePosixPath: 165 """Overrides :meth:`~.os_session.OSSession.create_remote_tarball`.""" 166 167 def generate_tar_exclude_args(exclude_patterns) -> str: 168 """Generate args to exclude patterns when creating a tarball. 169 170 Args: 171 exclude_patterns: Patterns for files or directories to exclude from the tarball. 172 These patterns are used with `tar`'s `--exclude` option. 173 174 Returns: 175 The generated string args to exclude the specified patterns. 176 """ 177 if exclude_patterns: 178 exclude_patterns = convert_to_list_of_string(exclude_patterns) 179 return "".join([f" --exclude={pattern}" for pattern in exclude_patterns]) 180 return "" 181 182 posix_remote_dir_path = PurePosixPath(remote_dir_path) 183 target_tarball_path = PurePosixPath(f"{remote_dir_path}.{compress_format.extension}") 184 185 self.send_command( 186 f"tar caf {target_tarball_path}{generate_tar_exclude_args(exclude)} " 187 f"-C {posix_remote_dir_path.parent} {posix_remote_dir_path.name}", 188 60, 189 ) 190 191 return target_tarball_path 192 193 def extract_remote_tarball( 194 self, remote_tarball_path: str | PurePath, expected_dir: str | PurePath | None = None 195 ) -> None: 196 """Overrides :meth:`~.os_session.OSSession.extract_remote_tarball`.""" 197 self.send_command( 198 f"tar xfm {remote_tarball_path} -C {PurePosixPath(remote_tarball_path).parent}", 199 60, 200 ) 201 if expected_dir: 202 self.send_command(f"ls {expected_dir}", verify=True) 203 204 def is_remote_dir(self, remote_path: PurePath) -> bool: 205 """Overrides :meth:`~.os_session.OSSession.is_remote_dir`.""" 206 result = self.send_command(f"test -d {remote_path}") 207 return not result.return_code 208 209 def is_remote_tarfile(self, remote_tarball_path: PurePath) -> bool: 210 """Overrides :meth:`~.os_session.OSSession.is_remote_tarfile`.""" 211 result = self.send_command(f"tar -tvf {remote_tarball_path}") 212 return not result.return_code 213 214 def get_tarball_top_dir( 215 self, remote_tarball_path: str | PurePath 216 ) -> str | PurePosixPath | None: 217 """Overrides :meth:`~.os_session.OSSession.get_tarball_top_dir`.""" 218 members = self.send_command(f"tar tf {remote_tarball_path}").stdout.split() 219 220 top_dirs = [] 221 for member in members: 222 parts_of_member = PurePosixPath(member).parts 223 if parts_of_member: 224 top_dirs.append(parts_of_member[0]) 225 226 if len(set(top_dirs)) == 1: 227 return top_dirs[0] 228 return None 229 230 def build_dpdk( 231 self, 232 env_vars: dict, 233 meson_args: MesonArgs, 234 remote_dpdk_dir: str | PurePath, 235 remote_dpdk_build_dir: str | PurePath, 236 rebuild: bool = False, 237 timeout: float = SETTINGS.compile_timeout, 238 ) -> None: 239 """Overrides :meth:`~.os_session.OSSession.build_dpdk`.""" 240 try: 241 if rebuild: 242 # reconfigure, then build 243 self._logger.info("Reconfiguring DPDK build.") 244 self.send_command( 245 f"meson configure {meson_args} {remote_dpdk_build_dir}", 246 timeout, 247 verify=True, 248 env=env_vars, 249 ) 250 else: 251 # fresh build - remove target dir first, then build from scratch 252 self._logger.info("Configuring DPDK build from scratch.") 253 self.remove_remote_dir(remote_dpdk_build_dir) 254 self.send_command( 255 f"meson setup {meson_args} {remote_dpdk_dir} {remote_dpdk_build_dir}", 256 timeout, 257 verify=True, 258 env=env_vars, 259 ) 260 261 self._logger.info("Building DPDK.") 262 self.send_command( 263 f"ninja -C {remote_dpdk_build_dir}", timeout, verify=True, env=env_vars 264 ) 265 except RemoteCommandExecutionError as e: 266 raise DPDKBuildError(f"DPDK build failed when doing '{e.command}'.") 267 268 def get_dpdk_version(self, build_dir: str | PurePath) -> str: 269 """Overrides :meth:`~.os_session.OSSession.get_dpdk_version`.""" 270 out = self.send_command(f"cat {self.join_remote_path(build_dir, 'VERSION')}", verify=True) 271 return out.stdout 272 273 def kill_cleanup_dpdk_apps(self, dpdk_prefix_list: Iterable[str]) -> None: 274 """Overrides :meth:`~.os_session.OSSession.kill_cleanup_dpdk_apps`.""" 275 self._logger.info("Cleaning up DPDK apps.") 276 dpdk_runtime_dirs = self._get_dpdk_runtime_dirs(dpdk_prefix_list) 277 if dpdk_runtime_dirs: 278 # kill and cleanup only if DPDK is running 279 dpdk_pids = self._get_dpdk_pids(dpdk_runtime_dirs) 280 for dpdk_pid in dpdk_pids: 281 self.send_command(f"kill -9 {dpdk_pid}", 20) 282 self._check_dpdk_hugepages(dpdk_runtime_dirs) 283 self._remove_dpdk_runtime_dirs(dpdk_runtime_dirs) 284 285 def _get_dpdk_runtime_dirs(self, dpdk_prefix_list: Iterable[str]) -> list[PurePosixPath]: 286 """Find runtime directories DPDK apps are currently using. 287 288 Args: 289 dpdk_prefix_list: The prefixes DPDK apps were started with. 290 291 Returns: 292 The paths of DPDK apps' runtime dirs. 293 """ 294 prefix = PurePosixPath("/var", "run", "dpdk") 295 if not dpdk_prefix_list: 296 remote_prefixes = self._list_remote_dirs(prefix) 297 if not remote_prefixes: 298 dpdk_prefix_list = [] 299 else: 300 dpdk_prefix_list = remote_prefixes 301 302 return [PurePosixPath(prefix, dpdk_prefix) for dpdk_prefix in dpdk_prefix_list] 303 304 def _list_remote_dirs(self, remote_path: str | PurePath) -> list[str] | None: 305 """Contents of remote_path. 306 307 Args: 308 remote_path: List the contents of this path. 309 310 Returns: 311 The contents of remote_path. If remote_path doesn't exist, return None. 312 """ 313 out = self.send_command(f"ls -l {remote_path} | awk '/^d/ {{print $NF}}'").stdout 314 if "No such file or directory" in out: 315 return None 316 else: 317 return out.splitlines() 318 319 def _get_dpdk_pids(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> list[int]: 320 """Find PIDs of running DPDK apps. 321 322 Look at each "config" file found in dpdk_runtime_dirs and find the PIDs of processes 323 that opened those file. 324 325 Args: 326 dpdk_runtime_dirs: The paths of DPDK apps' runtime dirs. 327 328 Returns: 329 The PIDs of running DPDK apps. 330 """ 331 pids = [] 332 pid_regex = r"p(\d+)" 333 for dpdk_runtime_dir in dpdk_runtime_dirs: 334 dpdk_config_file = PurePosixPath(dpdk_runtime_dir, "config") 335 if self.remote_path_exists(dpdk_config_file): 336 out = self.send_command(f"lsof -Fp {dpdk_config_file}").stdout 337 if out and "No such file or directory" not in out: 338 for out_line in out.splitlines(): 339 match = re.match(pid_regex, out_line) 340 if match: 341 pids.append(int(match.group(1))) 342 return pids 343 344 def _check_dpdk_hugepages(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> None: 345 """Check there aren't any leftover hugepages. 346 347 If any hugepages are found, emit a warning. The hugepages are investigated in the 348 "hugepage_info" file of dpdk_runtime_dirs. 349 350 Args: 351 dpdk_runtime_dirs: The paths of DPDK apps' runtime dirs. 352 """ 353 for dpdk_runtime_dir in dpdk_runtime_dirs: 354 hugepage_info = PurePosixPath(dpdk_runtime_dir, "hugepage_info") 355 if self.remote_path_exists(hugepage_info): 356 out = self.send_command(f"lsof -Fp {hugepage_info}").stdout 357 if out and "No such file or directory" not in out: 358 self._logger.warning("Some DPDK processes did not free hugepages.") 359 self._logger.warning("*******************************************") 360 self._logger.warning(out) 361 self._logger.warning("*******************************************") 362 363 def _remove_dpdk_runtime_dirs(self, dpdk_runtime_dirs: Iterable[str | PurePath]) -> None: 364 for dpdk_runtime_dir in dpdk_runtime_dirs: 365 self.remove_remote_dir(dpdk_runtime_dir) 366 367 def get_dpdk_file_prefix(self, dpdk_prefix: str) -> str: 368 """Overrides :meth:`~.os_session.OSSession.get_dpdk_file_prefix`.""" 369 return "" 370 371 def get_compiler_version(self, compiler_name: str) -> str: 372 """Overrides :meth:`~.os_session.OSSession.get_compiler_version`.""" 373 match compiler_name: 374 case "gcc": 375 return self.send_command( 376 f"{compiler_name} --version", SETTINGS.timeout 377 ).stdout.split("\n")[0] 378 case "clang": 379 return self.send_command( 380 f"{compiler_name} --version", SETTINGS.timeout 381 ).stdout.split("\n")[0] 382 case "msvc": 383 return self.send_command("cl", SETTINGS.timeout).stdout 384 case "icc": 385 return self.send_command(f"{compiler_name} -V", SETTINGS.timeout).stdout 386 case _: 387 raise ValueError(f"Unknown compiler {compiler_name}") 388 389 def get_node_info(self) -> OSSessionInfo: 390 """Overrides :meth:`~.os_session.OSSession.get_node_info`.""" 391 os_release_info = self.send_command( 392 "awk -F= '$1 ~ /^NAME$|^VERSION$/ {print $2}' /etc/os-release", 393 SETTINGS.timeout, 394 ).stdout.split("\n") 395 kernel_version = self.send_command("uname -r", SETTINGS.timeout).stdout 396 return OSSessionInfo(os_release_info[0].strip(), os_release_info[1].strip(), kernel_version) 397