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 5a23f2245SLuca Vizzarro# Copyright(c) 2024 Arm Limited 6812c4071SJuraj Linkeš 76ef07151SJuraj Linkeš"""Various utility classes and functions. 86ef07151SJuraj Linkeš 96ef07151SJuraj LinkešThese are used in multiple modules across the framework. They're here because 106ef07151SJuraj Linkešthey provide some non-specific functionality, greatly simplify imports or just don't 116ef07151SJuraj Linkešfit elsewhere. 126ef07151SJuraj Linkeš 136ef07151SJuraj LinkešAttributes: 146ef07151SJuraj Linkeš REGEX_FOR_PCI_ADDRESS: The regex representing a PCI address, e.g. ``0000:00:08.0``. 156ef07151SJuraj Linkeš""" 166ef07151SJuraj Linkeš 17c9d31a7eSJuraj Linkešimport atexit 18cecfe0aaSJuraj Linkešimport json 19c9d31a7eSJuraj Linkešimport os 20f51557fbSLuca Vizzarroimport random 21c9d31a7eSJuraj Linkešimport subprocess 22f51557fbSLuca Vizzarrofrom enum import Enum, Flag 23c9d31a7eSJuraj Linkešfrom pathlib import Path 24c9d31a7eSJuraj Linkešfrom subprocess import SubprocessError 25c9d31a7eSJuraj Linkeš 26f51557fbSLuca Vizzarrofrom scapy.layers.inet import IP, TCP, UDP, Ether # type: ignore[import-untyped] 27282688eaSLuca Vizzarrofrom scapy.packet import Packet # type: ignore[import-untyped] 28cecfe0aaSJuraj Linkeš 29f51557fbSLuca Vizzarrofrom .exception import ConfigurationError, InternalError 30c9d31a7eSJuraj Linkeš 31840b1e01SJuraj LinkešREGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" 32*a91d5f47SJeremy Spewock_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC: str = r"(?:[\da-fA-F]{2}[:-]){5}[\da-fA-F]{2}" 33*a91d5f47SJeremy Spewock_REGEX_FOR_DOT_SEP_MAC: str = r"(?:[\da-fA-F]{4}.){2}[\da-fA-F]{4}" 34*a91d5f47SJeremy SpewockREGEX_FOR_MAC_ADDRESS: str = rf"{_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC}|{_REGEX_FOR_DOT_SEP_MAC}" 3599740300SJeremy SpewockREGEX_FOR_BASE64_ENCODING: str = "[-a-zA-Z0-9+\\/]*={0,3}" 3657c58bf8SJuraj Linkeš 37812c4071SJuraj Linkeš 38c020b7ceSJuraj Linkešdef expand_range(range_str: str) -> list[int]: 396ef07151SJuraj Linkeš """Process `range_str` into a list of integers. 40c020b7ceSJuraj Linkeš 416ef07151SJuraj Linkeš There are two possible formats of `range_str`: 426ef07151SJuraj Linkeš 436ef07151SJuraj Linkeš * ``n`` - a single integer, 446ef07151SJuraj Linkeš * ``n-m`` - a range of integers. 456ef07151SJuraj Linkeš 466ef07151SJuraj Linkeš The returned range includes both ``n`` and ``m``. Empty string returns an empty list. 476ef07151SJuraj Linkeš 486ef07151SJuraj Linkeš Args: 496ef07151SJuraj Linkeš range_str: The range to expand. 506ef07151SJuraj Linkeš 516ef07151SJuraj Linkeš Returns: 526ef07151SJuraj Linkeš All the numbers from the range. 53c020b7ceSJuraj Linkeš """ 54c020b7ceSJuraj Linkeš expanded_range: list[int] = [] 55c020b7ceSJuraj Linkeš if range_str: 56c020b7ceSJuraj Linkeš range_boundaries = range_str.split("-") 57c020b7ceSJuraj Linkeš # will throw an exception when items in range_boundaries can't be converted, 58c020b7ceSJuraj Linkeš # serving as type check 59517b4b26SJuraj Linkeš expanded_range.extend(range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)) 60c020b7ceSJuraj Linkeš 61c020b7ceSJuraj Linkeš return expanded_range 62c020b7ceSJuraj Linkeš 63c020b7ceSJuraj Linkeš 64840b1e01SJuraj Linkešdef get_packet_summaries(packets: list[Packet]) -> str: 656ef07151SJuraj Linkeš """Format a string summary from `packets`. 666ef07151SJuraj Linkeš 676ef07151SJuraj Linkeš Args: 686ef07151SJuraj Linkeš packets: The packets to format. 696ef07151SJuraj Linkeš 706ef07151SJuraj Linkeš Returns: 716ef07151SJuraj Linkeš The summary of `packets`. 726ef07151SJuraj Linkeš """ 73cecfe0aaSJuraj Linkeš if len(packets) == 1: 74cecfe0aaSJuraj Linkeš packet_summaries = packets[0].summary() 75cecfe0aaSJuraj Linkeš else: 76517b4b26SJuraj Linkeš packet_summaries = json.dumps(list(map(lambda pkt: pkt.summary(), packets)), indent=4) 77cecfe0aaSJuraj Linkeš return f"Packet contents: \n{packet_summaries}" 78cecfe0aaSJuraj Linkeš 79cecfe0aaSJuraj Linkeš 80a23f2245SLuca Vizzarrodef get_commit_id(rev_id: str) -> str: 81a23f2245SLuca Vizzarro """Given a Git revision ID, return the corresponding commit ID. 82a23f2245SLuca Vizzarro 83a23f2245SLuca Vizzarro Args: 84a23f2245SLuca Vizzarro rev_id: The Git revision ID. 85a23f2245SLuca Vizzarro 86a23f2245SLuca Vizzarro Raises: 87a23f2245SLuca Vizzarro ConfigurationError: The ``git rev-parse`` command failed, suggesting 88a23f2245SLuca Vizzarro an invalid or ambiguous revision ID was supplied. 89a23f2245SLuca Vizzarro """ 90a23f2245SLuca Vizzarro result = subprocess.run( 91a23f2245SLuca Vizzarro ["git", "rev-parse", "--verify", rev_id], 92a23f2245SLuca Vizzarro text=True, 93a23f2245SLuca Vizzarro capture_output=True, 94a23f2245SLuca Vizzarro ) 95a23f2245SLuca Vizzarro if result.returncode != 0: 96a23f2245SLuca Vizzarro raise ConfigurationError( 97a23f2245SLuca Vizzarro f"{rev_id} is not a valid git reference.\n" 98a23f2245SLuca Vizzarro f"Command: {result.args}\n" 99a23f2245SLuca Vizzarro f"Stdout: {result.stdout}\n" 100a23f2245SLuca Vizzarro f"Stderr: {result.stderr}" 101a23f2245SLuca Vizzarro ) 102a23f2245SLuca Vizzarro return result.stdout.strip() 103a23f2245SLuca Vizzarro 104a23f2245SLuca Vizzarro 105840b1e01SJuraj Linkešclass StrEnum(Enum): 1066ef07151SJuraj Linkeš """Enum with members stored as strings.""" 1076ef07151SJuraj Linkeš 108840b1e01SJuraj Linkeš @staticmethod 109840b1e01SJuraj Linkeš def _generate_next_value_(name: str, start: int, count: int, last_values: object) -> str: 110840b1e01SJuraj Linkeš return name 111840b1e01SJuraj Linkeš 112840b1e01SJuraj Linkeš def __str__(self) -> str: 1136ef07151SJuraj Linkeš """The string representation is the name of the member.""" 114840b1e01SJuraj Linkeš return self.name 115680d8a24SJuraj Linkeš 116680d8a24SJuraj Linkeš 1173e967643SJuraj Linkešclass MesonArgs: 1186ef07151SJuraj Linkeš """Aggregate the arguments needed to build DPDK.""" 119680d8a24SJuraj Linkeš 120680d8a24SJuraj Linkeš _default_library: str 121680d8a24SJuraj Linkeš 122680d8a24SJuraj Linkeš def __init__(self, default_library: str | None = None, **dpdk_args: str | bool): 1236ef07151SJuraj Linkeš """Initialize the meson arguments. 1246ef07151SJuraj Linkeš 1256ef07151SJuraj Linkeš Args: 1266ef07151SJuraj Linkeš default_library: The default library type, Meson supports ``shared``, ``static`` and 1276ef07151SJuraj Linkeš ``both``. Defaults to :data:`None`, in which case the argument won't be used. 1286ef07151SJuraj Linkeš dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory. 1296ef07151SJuraj Linkeš Do not use ``-D`` with them. 1306ef07151SJuraj Linkeš 1316ef07151SJuraj Linkeš Example: 1326ef07151SJuraj Linkeš :: 1336ef07151SJuraj Linkeš 1346ef07151SJuraj Linkeš meson_args = MesonArgs(enable_kmods=True). 1356ef07151SJuraj Linkeš """ 136517b4b26SJuraj Linkeš self._default_library = f"--default-library={default_library}" if default_library else "" 137680d8a24SJuraj Linkeš self._dpdk_args = " ".join( 138680d8a24SJuraj Linkeš ( 139680d8a24SJuraj Linkeš f"-D{dpdk_arg_name}={dpdk_arg_value}" 140680d8a24SJuraj Linkeš for dpdk_arg_name, dpdk_arg_value in dpdk_args.items() 141680d8a24SJuraj Linkeš ) 142680d8a24SJuraj Linkeš ) 143680d8a24SJuraj Linkeš 144680d8a24SJuraj Linkeš def __str__(self) -> str: 1456ef07151SJuraj Linkeš """The actual args.""" 146680d8a24SJuraj Linkeš return " ".join(f"{self._default_library} {self._dpdk_args}".split()) 147c9d31a7eSJuraj Linkeš 148c9d31a7eSJuraj Linkeš 149c9d31a7eSJuraj Linkešclass _TarCompressionFormat(StrEnum): 150c9d31a7eSJuraj Linkeš """Compression formats that tar can use. 151c9d31a7eSJuraj Linkeš 152c9d31a7eSJuraj Linkeš Enum names are the shell compression commands 153c9d31a7eSJuraj Linkeš and Enum values are the associated file extensions. 154c9d31a7eSJuraj Linkeš """ 155c9d31a7eSJuraj Linkeš 156c9d31a7eSJuraj Linkeš gzip = "gz" 157c9d31a7eSJuraj Linkeš compress = "Z" 158c9d31a7eSJuraj Linkeš bzip2 = "bz2" 159c9d31a7eSJuraj Linkeš lzip = "lz" 160c9d31a7eSJuraj Linkeš lzma = "lzma" 161c9d31a7eSJuraj Linkeš lzop = "lzo" 162c9d31a7eSJuraj Linkeš xz = "xz" 163c9d31a7eSJuraj Linkeš zstd = "zst" 164c9d31a7eSJuraj Linkeš 165c9d31a7eSJuraj Linkeš 1663e967643SJuraj Linkešclass DPDKGitTarball: 1676ef07151SJuraj Linkeš """Compressed tarball of DPDK from the repository. 168c9d31a7eSJuraj Linkeš 1696ef07151SJuraj Linkeš The class supports the :class:`os.PathLike` protocol, 170c9d31a7eSJuraj Linkeš which is used to get the Path of the tarball:: 171c9d31a7eSJuraj Linkeš 172c9d31a7eSJuraj Linkeš from pathlib import Path 173c9d31a7eSJuraj Linkeš tarball = DPDKGitTarball("HEAD", "output") 174c9d31a7eSJuraj Linkeš tarball_path = Path(tarball) 175c9d31a7eSJuraj Linkeš """ 176c9d31a7eSJuraj Linkeš 177c9d31a7eSJuraj Linkeš _git_ref: str 178c9d31a7eSJuraj Linkeš _tar_compression_format: _TarCompressionFormat 179c9d31a7eSJuraj Linkeš _tarball_dir: Path 180c9d31a7eSJuraj Linkeš _tarball_name: str 181c9d31a7eSJuraj Linkeš _tarball_path: Path | None 182c9d31a7eSJuraj Linkeš 183c9d31a7eSJuraj Linkeš def __init__( 184c9d31a7eSJuraj Linkeš self, 185c9d31a7eSJuraj Linkeš git_ref: str, 186c9d31a7eSJuraj Linkeš output_dir: str, 187c9d31a7eSJuraj Linkeš tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz, 188c9d31a7eSJuraj Linkeš ): 1896ef07151SJuraj Linkeš """Create the tarball during initialization. 1906ef07151SJuraj Linkeš 1916ef07151SJuraj Linkeš The DPDK version is specified with `git_ref`. The tarball will be compressed with 1926ef07151SJuraj Linkeš `tar_compression_format`, which must be supported by the DTS execution environment. 1936ef07151SJuraj Linkeš The resulting tarball will be put into `output_dir`. 1946ef07151SJuraj Linkeš 1956ef07151SJuraj Linkeš Args: 1966ef07151SJuraj Linkeš git_ref: A git commit ID, tag ID or tree ID. 1976ef07151SJuraj Linkeš output_dir: The directory where to put the resulting tarball. 1986ef07151SJuraj Linkeš tar_compression_format: The compression format to use. 1996ef07151SJuraj Linkeš """ 200c9d31a7eSJuraj Linkeš self._git_ref = git_ref 201c9d31a7eSJuraj Linkeš self._tar_compression_format = tar_compression_format 202c9d31a7eSJuraj Linkeš 203c9d31a7eSJuraj Linkeš self._tarball_dir = Path(output_dir, "tarball") 204c9d31a7eSJuraj Linkeš 205c9d31a7eSJuraj Linkeš self._create_tarball_dir() 206c9d31a7eSJuraj Linkeš 207c9d31a7eSJuraj Linkeš self._tarball_name = ( 208c9d31a7eSJuraj Linkeš f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}" 209c9d31a7eSJuraj Linkeš ) 210c9d31a7eSJuraj Linkeš self._tarball_path = self._check_tarball_path() 211c9d31a7eSJuraj Linkeš if not self._tarball_path: 212c9d31a7eSJuraj Linkeš self._create_tarball() 213c9d31a7eSJuraj Linkeš 214c9d31a7eSJuraj Linkeš def _create_tarball_dir(self) -> None: 215c9d31a7eSJuraj Linkeš os.makedirs(self._tarball_dir, exist_ok=True) 216c9d31a7eSJuraj Linkeš 217c9d31a7eSJuraj Linkeš def _check_tarball_path(self) -> Path | None: 218c9d31a7eSJuraj Linkeš if self._tarball_name in os.listdir(self._tarball_dir): 219c9d31a7eSJuraj Linkeš return Path(self._tarball_dir, self._tarball_name) 220c9d31a7eSJuraj Linkeš return None 221c9d31a7eSJuraj Linkeš 222c9d31a7eSJuraj Linkeš def _create_tarball(self) -> None: 223c9d31a7eSJuraj Linkeš self._tarball_path = Path(self._tarball_dir, self._tarball_name) 224c9d31a7eSJuraj Linkeš 225c9d31a7eSJuraj Linkeš atexit.register(self._delete_tarball) 226c9d31a7eSJuraj Linkeš 227c9d31a7eSJuraj Linkeš result = subprocess.run( 228c9d31a7eSJuraj Linkeš 'git -C "$(git rev-parse --show-toplevel)" archive ' 229c9d31a7eSJuraj Linkeš f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | ' 230c9d31a7eSJuraj Linkeš f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}", 231c9d31a7eSJuraj Linkeš shell=True, 232c9d31a7eSJuraj Linkeš text=True, 233c9d31a7eSJuraj Linkeš capture_output=True, 234c9d31a7eSJuraj Linkeš ) 235c9d31a7eSJuraj Linkeš 236c9d31a7eSJuraj Linkeš if result.returncode != 0: 237c9d31a7eSJuraj Linkeš raise SubprocessError( 238c9d31a7eSJuraj Linkeš f"Git archive creation failed with exit code {result.returncode}.\n" 239c9d31a7eSJuraj Linkeš f"Command: {result.args}\n" 240c9d31a7eSJuraj Linkeš f"Stdout: {result.stdout}\n" 241c9d31a7eSJuraj Linkeš f"Stderr: {result.stderr}" 242c9d31a7eSJuraj Linkeš ) 243c9d31a7eSJuraj Linkeš 244c9d31a7eSJuraj Linkeš atexit.unregister(self._delete_tarball) 245c9d31a7eSJuraj Linkeš 246c9d31a7eSJuraj Linkeš def _delete_tarball(self) -> None: 247c9d31a7eSJuraj Linkeš if self._tarball_path and os.path.exists(self._tarball_path): 248c9d31a7eSJuraj Linkeš os.remove(self._tarball_path) 249c9d31a7eSJuraj Linkeš 250840b1e01SJuraj Linkeš def __fspath__(self) -> str: 2516ef07151SJuraj Linkeš """The os.PathLike protocol implementation.""" 252c9d31a7eSJuraj Linkeš return str(self._tarball_path) 253f51557fbSLuca Vizzarro 254f51557fbSLuca Vizzarro 255f51557fbSLuca Vizzarroclass PacketProtocols(Flag): 256f51557fbSLuca Vizzarro """Flag specifying which protocols to use for packet generation.""" 257f51557fbSLuca Vizzarro 258f51557fbSLuca Vizzarro #: 259f51557fbSLuca Vizzarro IP = 1 260f51557fbSLuca Vizzarro #: 261f51557fbSLuca Vizzarro TCP = 2 | IP 262f51557fbSLuca Vizzarro #: 263f51557fbSLuca Vizzarro UDP = 4 | IP 264f51557fbSLuca Vizzarro #: 265f51557fbSLuca Vizzarro ALL = TCP | UDP 266f51557fbSLuca Vizzarro 267f51557fbSLuca Vizzarro 268f51557fbSLuca Vizzarrodef generate_random_packets( 269f51557fbSLuca Vizzarro number_of: int, 270f51557fbSLuca Vizzarro payload_size: int = 1500, 271f51557fbSLuca Vizzarro protocols: PacketProtocols = PacketProtocols.ALL, 272f51557fbSLuca Vizzarro ports_range: range = range(1024, 49152), 273f51557fbSLuca Vizzarro mtu: int = 1500, 274f51557fbSLuca Vizzarro) -> list[Packet]: 275f51557fbSLuca Vizzarro """Generate a number of random packets. 276f51557fbSLuca Vizzarro 277f51557fbSLuca Vizzarro The payload of the packets will consist of random bytes. If `payload_size` is too big, then the 278f51557fbSLuca Vizzarro maximum payload size allowed for the specific packet type is used. The size is calculated based 279f51557fbSLuca Vizzarro on the specified `mtu`, therefore it is essential that `mtu` is set correctly to match the MTU 280f51557fbSLuca Vizzarro of the port that will send out the generated packets. 281f51557fbSLuca Vizzarro 282f51557fbSLuca Vizzarro If `protocols` has any L4 protocol enabled then all the packets are generated with any of 283f51557fbSLuca Vizzarro the specified L4 protocols chosen at random. If only :attr:`~PacketProtocols.IP` is set, then 284f51557fbSLuca Vizzarro only L3 packets are generated. 285f51557fbSLuca Vizzarro 286f51557fbSLuca Vizzarro If L4 packets will be generated, then the TCP/UDP ports to be used will be chosen at random from 287f51557fbSLuca Vizzarro `ports_range`. 288f51557fbSLuca Vizzarro 289f51557fbSLuca Vizzarro Args: 290f51557fbSLuca Vizzarro number_of: The number of packets to generate. 291f51557fbSLuca Vizzarro payload_size: The packet payload size to generate, capped based on `mtu`. 292f51557fbSLuca Vizzarro protocols: The protocols to use for the generated packets. 293f51557fbSLuca Vizzarro ports_range: The range of L4 port numbers to use. Used only if `protocols` has L4 protocols. 294f51557fbSLuca Vizzarro mtu: The MTU of the NIC port that will send out the generated packets. 295f51557fbSLuca Vizzarro 296f51557fbSLuca Vizzarro Raises: 297f51557fbSLuca Vizzarro InternalError: If the `payload_size` is invalid. 298f51557fbSLuca Vizzarro 299f51557fbSLuca Vizzarro Returns: 300f51557fbSLuca Vizzarro A list containing the randomly generated packets. 301f51557fbSLuca Vizzarro """ 302f51557fbSLuca Vizzarro if payload_size < 0: 303f51557fbSLuca Vizzarro raise InternalError(f"An invalid payload_size of {payload_size} was given.") 304f51557fbSLuca Vizzarro 305f51557fbSLuca Vizzarro l4_factories = [] 306f51557fbSLuca Vizzarro if protocols & PacketProtocols.TCP: 307f51557fbSLuca Vizzarro l4_factories.append(TCP) 308f51557fbSLuca Vizzarro if protocols & PacketProtocols.UDP: 309f51557fbSLuca Vizzarro l4_factories.append(UDP) 310f51557fbSLuca Vizzarro 311f51557fbSLuca Vizzarro def _make_packet() -> Packet: 312f51557fbSLuca Vizzarro packet = Ether() 313f51557fbSLuca Vizzarro 314f51557fbSLuca Vizzarro if protocols & PacketProtocols.IP: 315f51557fbSLuca Vizzarro packet /= IP() 316f51557fbSLuca Vizzarro 317f51557fbSLuca Vizzarro if len(l4_factories) > 0: 318f51557fbSLuca Vizzarro src_port, dst_port = random.choices(ports_range, k=2) 319f51557fbSLuca Vizzarro packet /= random.choice(l4_factories)(sport=src_port, dport=dst_port) 320f51557fbSLuca Vizzarro 321f51557fbSLuca Vizzarro max_payload_size = mtu - len(packet) 322f51557fbSLuca Vizzarro usable_payload_size = payload_size if payload_size < max_payload_size else max_payload_size 323f51557fbSLuca Vizzarro return packet / random.randbytes(usable_payload_size) 324f51557fbSLuca Vizzarro 325f51557fbSLuca Vizzarro return [_make_packet() for _ in range(number_of)] 32699740300SJeremy Spewock 32799740300SJeremy Spewock 32899740300SJeremy Spewockclass MultiInheritanceBaseClass: 32999740300SJeremy Spewock """A base class for classes utilizing multiple inheritance. 33099740300SJeremy Spewock 33199740300SJeremy Spewock This class enables it's subclasses to support both single and multiple inheritance by acting as 33299740300SJeremy Spewock a stopping point in the tree of calls to the constructors of superclasses. This class is able 33399740300SJeremy Spewock to exist at the end of the Method Resolution Order (MRO) so that subclasses can call 33499740300SJeremy Spewock :meth:`super.__init__` without repercussion. 33599740300SJeremy Spewock """ 33699740300SJeremy Spewock 33799740300SJeremy Spewock def __init__(self, *args, **kwargs) -> None: 33899740300SJeremy Spewock """Call the init method of :class:`object`.""" 33999740300SJeremy Spewock super().__init__() 340