1# SPDX-License-Identifier: BSD-3-Clause 2# Copyright(c) 2010-2014 Intel Corporation 3# Copyright(c) 2022-2023 PANTHEON.tech s.r.o. 4# Copyright(c) 2022-2023 University of New Hampshire 5 6import atexit 7import json 8import os 9import subprocess 10import sys 11from enum import Enum 12from pathlib import Path 13from subprocess import SubprocessError 14 15from scapy.packet import Packet # type: ignore[import] 16 17from .exception import ConfigurationError 18 19 20class StrEnum(Enum): 21 @staticmethod 22 def _generate_next_value_( 23 name: str, start: int, count: int, last_values: object 24 ) -> str: 25 return name 26 27 def __str__(self) -> str: 28 return self.name 29 30 31REGEX_FOR_PCI_ADDRESS = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" 32 33 34def check_dts_python_version() -> None: 35 if sys.version_info.major < 3 or ( 36 sys.version_info.major == 3 and sys.version_info.minor < 10 37 ): 38 print( 39 RED( 40 ( 41 "WARNING: DTS execution node's python version is lower than" 42 "python 3.10, is deprecated and will not work in future releases." 43 ) 44 ), 45 file=sys.stderr, 46 ) 47 print(RED("Please use Python >= 3.10 instead"), file=sys.stderr) 48 49 50def expand_range(range_str: str) -> list[int]: 51 """ 52 Process range string into a list of integers. There are two possible formats: 53 n - a single integer 54 n-m - a range of integers 55 56 The returned range includes both n and m. Empty string returns an empty list. 57 """ 58 expanded_range: list[int] = [] 59 if range_str: 60 range_boundaries = range_str.split("-") 61 # will throw an exception when items in range_boundaries can't be converted, 62 # serving as type check 63 expanded_range.extend( 64 range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1) 65 ) 66 67 return expanded_range 68 69 70def get_packet_summaries(packets: list[Packet]): 71 if len(packets) == 1: 72 packet_summaries = packets[0].summary() 73 else: 74 packet_summaries = json.dumps( 75 list(map(lambda pkt: pkt.summary(), packets)), indent=4 76 ) 77 return f"Packet contents: \n{packet_summaries}" 78 79 80def RED(text: str) -> str: 81 return f"\u001B[31;1m{str(text)}\u001B[0m" 82 83 84class MesonArgs(object): 85 """ 86 Aggregate the arguments needed to build DPDK: 87 default_library: Default library type, Meson allows "shared", "static" and "both". 88 Defaults to None, in which case the argument won't be used. 89 Keyword arguments: The arguments found in meson_options.txt in root DPDK directory. 90 Do not use -D with them, for example: 91 meson_args = MesonArgs(enable_kmods=True). 92 """ 93 94 _default_library: str 95 96 def __init__(self, default_library: str | None = None, **dpdk_args: str | bool): 97 self._default_library = ( 98 f"--default-library={default_library}" if default_library else "" 99 ) 100 self._dpdk_args = " ".join( 101 ( 102 f"-D{dpdk_arg_name}={dpdk_arg_value}" 103 for dpdk_arg_name, dpdk_arg_value in dpdk_args.items() 104 ) 105 ) 106 107 def __str__(self) -> str: 108 return " ".join(f"{self._default_library} {self._dpdk_args}".split()) 109 110 111class _TarCompressionFormat(StrEnum): 112 """Compression formats that tar can use. 113 114 Enum names are the shell compression commands 115 and Enum values are the associated file extensions. 116 """ 117 118 gzip = "gz" 119 compress = "Z" 120 bzip2 = "bz2" 121 lzip = "lz" 122 lzma = "lzma" 123 lzop = "lzo" 124 xz = "xz" 125 zstd = "zst" 126 127 128class DPDKGitTarball(object): 129 """Create a compressed tarball of DPDK from the repository. 130 131 The DPDK version is specified with git object git_ref. 132 The tarball will be compressed with _TarCompressionFormat, 133 which must be supported by the DTS execution environment. 134 The resulting tarball will be put into output_dir. 135 136 The class supports the os.PathLike protocol, 137 which is used to get the Path of the tarball:: 138 139 from pathlib import Path 140 tarball = DPDKGitTarball("HEAD", "output") 141 tarball_path = Path(tarball) 142 143 Arguments: 144 git_ref: A git commit ID, tag ID or tree ID. 145 output_dir: The directory where to put the resulting tarball. 146 tar_compression_format: The compression format to use. 147 """ 148 149 _git_ref: str 150 _tar_compression_format: _TarCompressionFormat 151 _tarball_dir: Path 152 _tarball_name: str 153 _tarball_path: Path | None 154 155 def __init__( 156 self, 157 git_ref: str, 158 output_dir: str, 159 tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz, 160 ): 161 self._git_ref = git_ref 162 self._tar_compression_format = tar_compression_format 163 164 self._tarball_dir = Path(output_dir, "tarball") 165 166 self._get_commit_id() 167 self._create_tarball_dir() 168 169 self._tarball_name = ( 170 f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}" 171 ) 172 self._tarball_path = self._check_tarball_path() 173 if not self._tarball_path: 174 self._create_tarball() 175 176 def _get_commit_id(self) -> None: 177 result = subprocess.run( 178 ["git", "rev-parse", "--verify", self._git_ref], 179 text=True, 180 capture_output=True, 181 ) 182 if result.returncode != 0: 183 raise ConfigurationError( 184 f"{self._git_ref} is neither a path to an existing DPDK " 185 "archive nor a valid git reference.\n" 186 f"Command: {result.args}\n" 187 f"Stdout: {result.stdout}\n" 188 f"Stderr: {result.stderr}" 189 ) 190 self._git_ref = result.stdout.strip() 191 192 def _create_tarball_dir(self) -> None: 193 os.makedirs(self._tarball_dir, exist_ok=True) 194 195 def _check_tarball_path(self) -> Path | None: 196 if self._tarball_name in os.listdir(self._tarball_dir): 197 return Path(self._tarball_dir, self._tarball_name) 198 return None 199 200 def _create_tarball(self) -> None: 201 self._tarball_path = Path(self._tarball_dir, self._tarball_name) 202 203 atexit.register(self._delete_tarball) 204 205 result = subprocess.run( 206 'git -C "$(git rev-parse --show-toplevel)" archive ' 207 f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | ' 208 f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}", 209 shell=True, 210 text=True, 211 capture_output=True, 212 ) 213 214 if result.returncode != 0: 215 raise SubprocessError( 216 f"Git archive creation failed with exit code {result.returncode}.\n" 217 f"Command: {result.args}\n" 218 f"Stdout: {result.stdout}\n" 219 f"Stderr: {result.stderr}" 220 ) 221 222 atexit.unregister(self._delete_tarball) 223 224 def _delete_tarball(self) -> None: 225 if self._tarball_path and os.path.exists(self._tarball_path): 226 os.remove(self._tarball_path) 227 228 def __fspath__(self): 229 return str(self._tarball_path) 230