1812c4071SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause 2812c4071SJuraj Linkeš# Copyright(c) 2010-2014 Intel Corporation 3680d8a24SJuraj Linkeš# Copyright(c) 2022-2023 PANTHEON.tech s.r.o. 4680d8a24SJuraj Linkeš# Copyright(c) 2022-2023 University of New Hampshire 5812c4071SJuraj Linkeš 6*6ef07151SJuraj Linkeš"""Various utility classes and functions. 7*6ef07151SJuraj Linkeš 8*6ef07151SJuraj LinkešThese are used in multiple modules across the framework. They're here because 9*6ef07151SJuraj Linkešthey provide some non-specific functionality, greatly simplify imports or just don't 10*6ef07151SJuraj Linkešfit elsewhere. 11*6ef07151SJuraj Linkeš 12*6ef07151SJuraj LinkešAttributes: 13*6ef07151SJuraj Linkeš REGEX_FOR_PCI_ADDRESS: The regex representing a PCI address, e.g. ``0000:00:08.0``. 14*6ef07151SJuraj Linkeš""" 15*6ef07151SJuraj Linkeš 16c9d31a7eSJuraj Linkešimport atexit 17cecfe0aaSJuraj Linkešimport json 18c9d31a7eSJuraj Linkešimport os 19c9d31a7eSJuraj Linkešimport subprocess 20c9d31a7eSJuraj Linkešfrom enum import Enum 21c9d31a7eSJuraj Linkešfrom pathlib import Path 22c9d31a7eSJuraj Linkešfrom subprocess import SubprocessError 23c9d31a7eSJuraj Linkeš 24cecfe0aaSJuraj Linkešfrom scapy.packet import Packet # type: ignore[import] 25cecfe0aaSJuraj Linkeš 26c9d31a7eSJuraj Linkešfrom .exception import ConfigurationError 27c9d31a7eSJuraj Linkeš 28840b1e01SJuraj LinkešREGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" 2957c58bf8SJuraj Linkeš 30812c4071SJuraj Linkeš 31c020b7ceSJuraj Linkešdef expand_range(range_str: str) -> list[int]: 32*6ef07151SJuraj Linkeš """Process `range_str` into a list of integers. 33c020b7ceSJuraj Linkeš 34*6ef07151SJuraj Linkeš There are two possible formats of `range_str`: 35*6ef07151SJuraj Linkeš 36*6ef07151SJuraj Linkeš * ``n`` - a single integer, 37*6ef07151SJuraj Linkeš * ``n-m`` - a range of integers. 38*6ef07151SJuraj Linkeš 39*6ef07151SJuraj Linkeš The returned range includes both ``n`` and ``m``. Empty string returns an empty list. 40*6ef07151SJuraj Linkeš 41*6ef07151SJuraj Linkeš Args: 42*6ef07151SJuraj Linkeš range_str: The range to expand. 43*6ef07151SJuraj Linkeš 44*6ef07151SJuraj Linkeš Returns: 45*6ef07151SJuraj Linkeš All the numbers from the range. 46c020b7ceSJuraj Linkeš """ 47c020b7ceSJuraj Linkeš expanded_range: list[int] = [] 48c020b7ceSJuraj Linkeš if range_str: 49c020b7ceSJuraj Linkeš range_boundaries = range_str.split("-") 50c020b7ceSJuraj Linkeš # will throw an exception when items in range_boundaries can't be converted, 51c020b7ceSJuraj Linkeš # serving as type check 52517b4b26SJuraj Linkeš expanded_range.extend(range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)) 53c020b7ceSJuraj Linkeš 54c020b7ceSJuraj Linkeš return expanded_range 55c020b7ceSJuraj Linkeš 56c020b7ceSJuraj Linkeš 57840b1e01SJuraj Linkešdef get_packet_summaries(packets: list[Packet]) -> str: 58*6ef07151SJuraj Linkeš """Format a string summary from `packets`. 59*6ef07151SJuraj Linkeš 60*6ef07151SJuraj Linkeš Args: 61*6ef07151SJuraj Linkeš packets: The packets to format. 62*6ef07151SJuraj Linkeš 63*6ef07151SJuraj Linkeš Returns: 64*6ef07151SJuraj Linkeš The summary of `packets`. 65*6ef07151SJuraj Linkeš """ 66cecfe0aaSJuraj Linkeš if len(packets) == 1: 67cecfe0aaSJuraj Linkeš packet_summaries = packets[0].summary() 68cecfe0aaSJuraj Linkeš else: 69517b4b26SJuraj Linkeš packet_summaries = json.dumps(list(map(lambda pkt: pkt.summary(), packets)), indent=4) 70cecfe0aaSJuraj Linkeš return f"Packet contents: \n{packet_summaries}" 71cecfe0aaSJuraj Linkeš 72cecfe0aaSJuraj Linkeš 73840b1e01SJuraj Linkešclass StrEnum(Enum): 74*6ef07151SJuraj Linkeš """Enum with members stored as strings.""" 75*6ef07151SJuraj Linkeš 76840b1e01SJuraj Linkeš @staticmethod 77840b1e01SJuraj Linkeš def _generate_next_value_(name: str, start: int, count: int, last_values: object) -> str: 78840b1e01SJuraj Linkeš return name 79840b1e01SJuraj Linkeš 80840b1e01SJuraj Linkeš def __str__(self) -> str: 81*6ef07151SJuraj Linkeš """The string representation is the name of the member.""" 82840b1e01SJuraj Linkeš return self.name 83680d8a24SJuraj Linkeš 84680d8a24SJuraj Linkeš 85680d8a24SJuraj Linkešclass MesonArgs(object): 86*6ef07151SJuraj Linkeš """Aggregate the arguments needed to build DPDK.""" 87680d8a24SJuraj Linkeš 88680d8a24SJuraj Linkeš _default_library: str 89680d8a24SJuraj Linkeš 90680d8a24SJuraj Linkeš def __init__(self, default_library: str | None = None, **dpdk_args: str | bool): 91*6ef07151SJuraj Linkeš """Initialize the meson arguments. 92*6ef07151SJuraj Linkeš 93*6ef07151SJuraj Linkeš Args: 94*6ef07151SJuraj Linkeš default_library: The default library type, Meson supports ``shared``, ``static`` and 95*6ef07151SJuraj Linkeš ``both``. Defaults to :data:`None`, in which case the argument won't be used. 96*6ef07151SJuraj Linkeš dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory. 97*6ef07151SJuraj Linkeš Do not use ``-D`` with them. 98*6ef07151SJuraj Linkeš 99*6ef07151SJuraj Linkeš Example: 100*6ef07151SJuraj Linkeš :: 101*6ef07151SJuraj Linkeš 102*6ef07151SJuraj Linkeš meson_args = MesonArgs(enable_kmods=True). 103*6ef07151SJuraj Linkeš """ 104517b4b26SJuraj Linkeš self._default_library = f"--default-library={default_library}" if default_library else "" 105680d8a24SJuraj Linkeš self._dpdk_args = " ".join( 106680d8a24SJuraj Linkeš ( 107680d8a24SJuraj Linkeš f"-D{dpdk_arg_name}={dpdk_arg_value}" 108680d8a24SJuraj Linkeš for dpdk_arg_name, dpdk_arg_value in dpdk_args.items() 109680d8a24SJuraj Linkeš ) 110680d8a24SJuraj Linkeš ) 111680d8a24SJuraj Linkeš 112680d8a24SJuraj Linkeš def __str__(self) -> str: 113*6ef07151SJuraj Linkeš """The actual args.""" 114680d8a24SJuraj Linkeš return " ".join(f"{self._default_library} {self._dpdk_args}".split()) 115c9d31a7eSJuraj Linkeš 116c9d31a7eSJuraj Linkeš 117c9d31a7eSJuraj Linkešclass _TarCompressionFormat(StrEnum): 118c9d31a7eSJuraj Linkeš """Compression formats that tar can use. 119c9d31a7eSJuraj Linkeš 120c9d31a7eSJuraj Linkeš Enum names are the shell compression commands 121c9d31a7eSJuraj Linkeš and Enum values are the associated file extensions. 122c9d31a7eSJuraj Linkeš """ 123c9d31a7eSJuraj Linkeš 124c9d31a7eSJuraj Linkeš gzip = "gz" 125c9d31a7eSJuraj Linkeš compress = "Z" 126c9d31a7eSJuraj Linkeš bzip2 = "bz2" 127c9d31a7eSJuraj Linkeš lzip = "lz" 128c9d31a7eSJuraj Linkeš lzma = "lzma" 129c9d31a7eSJuraj Linkeš lzop = "lzo" 130c9d31a7eSJuraj Linkeš xz = "xz" 131c9d31a7eSJuraj Linkeš zstd = "zst" 132c9d31a7eSJuraj Linkeš 133c9d31a7eSJuraj Linkeš 134c9d31a7eSJuraj Linkešclass DPDKGitTarball(object): 135*6ef07151SJuraj Linkeš """Compressed tarball of DPDK from the repository. 136c9d31a7eSJuraj Linkeš 137*6ef07151SJuraj Linkeš The class supports the :class:`os.PathLike` protocol, 138c9d31a7eSJuraj Linkeš which is used to get the Path of the tarball:: 139c9d31a7eSJuraj Linkeš 140c9d31a7eSJuraj Linkeš from pathlib import Path 141c9d31a7eSJuraj Linkeš tarball = DPDKGitTarball("HEAD", "output") 142c9d31a7eSJuraj Linkeš tarball_path = Path(tarball) 143c9d31a7eSJuraj Linkeš """ 144c9d31a7eSJuraj Linkeš 145c9d31a7eSJuraj Linkeš _git_ref: str 146c9d31a7eSJuraj Linkeš _tar_compression_format: _TarCompressionFormat 147c9d31a7eSJuraj Linkeš _tarball_dir: Path 148c9d31a7eSJuraj Linkeš _tarball_name: str 149c9d31a7eSJuraj Linkeš _tarball_path: Path | None 150c9d31a7eSJuraj Linkeš 151c9d31a7eSJuraj Linkeš def __init__( 152c9d31a7eSJuraj Linkeš self, 153c9d31a7eSJuraj Linkeš git_ref: str, 154c9d31a7eSJuraj Linkeš output_dir: str, 155c9d31a7eSJuraj Linkeš tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz, 156c9d31a7eSJuraj Linkeš ): 157*6ef07151SJuraj Linkeš """Create the tarball during initialization. 158*6ef07151SJuraj Linkeš 159*6ef07151SJuraj Linkeš The DPDK version is specified with `git_ref`. The tarball will be compressed with 160*6ef07151SJuraj Linkeš `tar_compression_format`, which must be supported by the DTS execution environment. 161*6ef07151SJuraj Linkeš The resulting tarball will be put into `output_dir`. 162*6ef07151SJuraj Linkeš 163*6ef07151SJuraj Linkeš Args: 164*6ef07151SJuraj Linkeš git_ref: A git commit ID, tag ID or tree ID. 165*6ef07151SJuraj Linkeš output_dir: The directory where to put the resulting tarball. 166*6ef07151SJuraj Linkeš tar_compression_format: The compression format to use. 167*6ef07151SJuraj Linkeš """ 168c9d31a7eSJuraj Linkeš self._git_ref = git_ref 169c9d31a7eSJuraj Linkeš self._tar_compression_format = tar_compression_format 170c9d31a7eSJuraj Linkeš 171c9d31a7eSJuraj Linkeš self._tarball_dir = Path(output_dir, "tarball") 172c9d31a7eSJuraj Linkeš 173c9d31a7eSJuraj Linkeš self._get_commit_id() 174c9d31a7eSJuraj Linkeš self._create_tarball_dir() 175c9d31a7eSJuraj Linkeš 176c9d31a7eSJuraj Linkeš self._tarball_name = ( 177c9d31a7eSJuraj Linkeš f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}" 178c9d31a7eSJuraj Linkeš ) 179c9d31a7eSJuraj Linkeš self._tarball_path = self._check_tarball_path() 180c9d31a7eSJuraj Linkeš if not self._tarball_path: 181c9d31a7eSJuraj Linkeš self._create_tarball() 182c9d31a7eSJuraj Linkeš 183c9d31a7eSJuraj Linkeš def _get_commit_id(self) -> None: 184c9d31a7eSJuraj Linkeš result = subprocess.run( 185c9d31a7eSJuraj Linkeš ["git", "rev-parse", "--verify", self._git_ref], 186c9d31a7eSJuraj Linkeš text=True, 187c9d31a7eSJuraj Linkeš capture_output=True, 188c9d31a7eSJuraj Linkeš ) 189c9d31a7eSJuraj Linkeš if result.returncode != 0: 190c9d31a7eSJuraj Linkeš raise ConfigurationError( 191c9d31a7eSJuraj Linkeš f"{self._git_ref} is neither a path to an existing DPDK " 192c9d31a7eSJuraj Linkeš "archive nor a valid git reference.\n" 193c9d31a7eSJuraj Linkeš f"Command: {result.args}\n" 194c9d31a7eSJuraj Linkeš f"Stdout: {result.stdout}\n" 195c9d31a7eSJuraj Linkeš f"Stderr: {result.stderr}" 196c9d31a7eSJuraj Linkeš ) 197c9d31a7eSJuraj Linkeš self._git_ref = result.stdout.strip() 198c9d31a7eSJuraj Linkeš 199c9d31a7eSJuraj Linkeš def _create_tarball_dir(self) -> None: 200c9d31a7eSJuraj Linkeš os.makedirs(self._tarball_dir, exist_ok=True) 201c9d31a7eSJuraj Linkeš 202c9d31a7eSJuraj Linkeš def _check_tarball_path(self) -> Path | None: 203c9d31a7eSJuraj Linkeš if self._tarball_name in os.listdir(self._tarball_dir): 204c9d31a7eSJuraj Linkeš return Path(self._tarball_dir, self._tarball_name) 205c9d31a7eSJuraj Linkeš return None 206c9d31a7eSJuraj Linkeš 207c9d31a7eSJuraj Linkeš def _create_tarball(self) -> None: 208c9d31a7eSJuraj Linkeš self._tarball_path = Path(self._tarball_dir, self._tarball_name) 209c9d31a7eSJuraj Linkeš 210c9d31a7eSJuraj Linkeš atexit.register(self._delete_tarball) 211c9d31a7eSJuraj Linkeš 212c9d31a7eSJuraj Linkeš result = subprocess.run( 213c9d31a7eSJuraj Linkeš 'git -C "$(git rev-parse --show-toplevel)" archive ' 214c9d31a7eSJuraj Linkeš f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | ' 215c9d31a7eSJuraj Linkeš f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}", 216c9d31a7eSJuraj Linkeš shell=True, 217c9d31a7eSJuraj Linkeš text=True, 218c9d31a7eSJuraj Linkeš capture_output=True, 219c9d31a7eSJuraj Linkeš ) 220c9d31a7eSJuraj Linkeš 221c9d31a7eSJuraj Linkeš if result.returncode != 0: 222c9d31a7eSJuraj Linkeš raise SubprocessError( 223c9d31a7eSJuraj Linkeš f"Git archive creation failed with exit code {result.returncode}.\n" 224c9d31a7eSJuraj Linkeš f"Command: {result.args}\n" 225c9d31a7eSJuraj Linkeš f"Stdout: {result.stdout}\n" 226c9d31a7eSJuraj Linkeš f"Stderr: {result.stderr}" 227c9d31a7eSJuraj Linkeš ) 228c9d31a7eSJuraj Linkeš 229c9d31a7eSJuraj Linkeš atexit.unregister(self._delete_tarball) 230c9d31a7eSJuraj Linkeš 231c9d31a7eSJuraj Linkeš def _delete_tarball(self) -> None: 232c9d31a7eSJuraj Linkeš if self._tarball_path and os.path.exists(self._tarball_path): 233c9d31a7eSJuraj Linkeš os.remove(self._tarball_path) 234c9d31a7eSJuraj Linkeš 235840b1e01SJuraj Linkeš def __fspath__(self) -> str: 236*6ef07151SJuraj Linkeš """The os.PathLike protocol implementation.""" 237c9d31a7eSJuraj Linkeš return str(self._tarball_path) 238