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# Copyright(c) 2024 Arm Limited 6 7"""Various utility classes and functions. 8 9These are used in multiple modules across the framework. They're here because 10they provide some non-specific functionality, greatly simplify imports or just don't 11fit elsewhere. 12 13Attributes: 14 REGEX_FOR_PCI_ADDRESS: The regex representing a PCI address, e.g. ``0000:00:08.0``. 15""" 16 17import atexit 18import json 19import os 20import random 21import subprocess 22from enum import Enum, Flag 23from pathlib import Path 24from subprocess import SubprocessError 25 26from scapy.layers.inet import IP, TCP, UDP, Ether # type: ignore[import-untyped] 27from scapy.packet import Packet # type: ignore[import-untyped] 28 29from .exception import ConfigurationError, InternalError 30 31REGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/" 32_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC: str = r"(?:[\da-fA-F]{2}[:-]){5}[\da-fA-F]{2}" 33_REGEX_FOR_DOT_SEP_MAC: str = r"(?:[\da-fA-F]{4}.){2}[\da-fA-F]{4}" 34REGEX_FOR_MAC_ADDRESS: str = rf"{_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC}|{_REGEX_FOR_DOT_SEP_MAC}" 35REGEX_FOR_BASE64_ENCODING: str = "[-a-zA-Z0-9+\\/]*={0,3}" 36 37 38def expand_range(range_str: str) -> list[int]: 39 """Process `range_str` into a list of integers. 40 41 There are two possible formats of `range_str`: 42 43 * ``n`` - a single integer, 44 * ``n-m`` - a range of integers. 45 46 The returned range includes both ``n`` and ``m``. Empty string returns an empty list. 47 48 Args: 49 range_str: The range to expand. 50 51 Returns: 52 All the numbers from the range. 53 """ 54 expanded_range: list[int] = [] 55 if range_str: 56 range_boundaries = range_str.split("-") 57 # will throw an exception when items in range_boundaries can't be converted, 58 # serving as type check 59 expanded_range.extend(range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)) 60 61 return expanded_range 62 63 64def get_packet_summaries(packets: list[Packet]) -> str: 65 """Format a string summary from `packets`. 66 67 Args: 68 packets: The packets to format. 69 70 Returns: 71 The summary of `packets`. 72 """ 73 if len(packets) == 1: 74 packet_summaries = packets[0].summary() 75 else: 76 packet_summaries = json.dumps(list(map(lambda pkt: pkt.summary(), packets)), indent=4) 77 return f"Packet contents: \n{packet_summaries}" 78 79 80def get_commit_id(rev_id: str) -> str: 81 """Given a Git revision ID, return the corresponding commit ID. 82 83 Args: 84 rev_id: The Git revision ID. 85 86 Raises: 87 ConfigurationError: The ``git rev-parse`` command failed, suggesting 88 an invalid or ambiguous revision ID was supplied. 89 """ 90 result = subprocess.run( 91 ["git", "rev-parse", "--verify", rev_id], 92 text=True, 93 capture_output=True, 94 ) 95 if result.returncode != 0: 96 raise ConfigurationError( 97 f"{rev_id} is not a valid git reference.\n" 98 f"Command: {result.args}\n" 99 f"Stdout: {result.stdout}\n" 100 f"Stderr: {result.stderr}" 101 ) 102 return result.stdout.strip() 103 104 105class StrEnum(Enum): 106 """Enum with members stored as strings.""" 107 108 @staticmethod 109 def _generate_next_value_(name: str, start: int, count: int, last_values: object) -> str: 110 return name 111 112 def __str__(self) -> str: 113 """The string representation is the name of the member.""" 114 return self.name 115 116 117class MesonArgs: 118 """Aggregate the arguments needed to build DPDK.""" 119 120 _default_library: str 121 122 def __init__(self, default_library: str | None = None, **dpdk_args: str | bool): 123 """Initialize the meson arguments. 124 125 Args: 126 default_library: The default library type, Meson supports ``shared``, ``static`` and 127 ``both``. Defaults to :data:`None`, in which case the argument won't be used. 128 dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory. 129 Do not use ``-D`` with them. 130 131 Example: 132 :: 133 134 meson_args = MesonArgs(enable_kmods=True). 135 """ 136 self._default_library = f"--default-library={default_library}" if default_library else "" 137 self._dpdk_args = " ".join( 138 ( 139 f"-D{dpdk_arg_name}={dpdk_arg_value}" 140 for dpdk_arg_name, dpdk_arg_value in dpdk_args.items() 141 ) 142 ) 143 144 def __str__(self) -> str: 145 """The actual args.""" 146 return " ".join(f"{self._default_library} {self._dpdk_args}".split()) 147 148 149class _TarCompressionFormat(StrEnum): 150 """Compression formats that tar can use. 151 152 Enum names are the shell compression commands 153 and Enum values are the associated file extensions. 154 """ 155 156 gzip = "gz" 157 compress = "Z" 158 bzip2 = "bz2" 159 lzip = "lz" 160 lzma = "lzma" 161 lzop = "lzo" 162 xz = "xz" 163 zstd = "zst" 164 165 166class DPDKGitTarball: 167 """Compressed tarball of DPDK from the repository. 168 169 The class supports the :class:`os.PathLike` protocol, 170 which is used to get the Path of the tarball:: 171 172 from pathlib import Path 173 tarball = DPDKGitTarball("HEAD", "output") 174 tarball_path = Path(tarball) 175 """ 176 177 _git_ref: str 178 _tar_compression_format: _TarCompressionFormat 179 _tarball_dir: Path 180 _tarball_name: str 181 _tarball_path: Path | None 182 183 def __init__( 184 self, 185 git_ref: str, 186 output_dir: str, 187 tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz, 188 ): 189 """Create the tarball during initialization. 190 191 The DPDK version is specified with `git_ref`. The tarball will be compressed with 192 `tar_compression_format`, which must be supported by the DTS execution environment. 193 The resulting tarball will be put into `output_dir`. 194 195 Args: 196 git_ref: A git commit ID, tag ID or tree ID. 197 output_dir: The directory where to put the resulting tarball. 198 tar_compression_format: The compression format to use. 199 """ 200 self._git_ref = git_ref 201 self._tar_compression_format = tar_compression_format 202 203 self._tarball_dir = Path(output_dir, "tarball") 204 205 self._create_tarball_dir() 206 207 self._tarball_name = ( 208 f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}" 209 ) 210 self._tarball_path = self._check_tarball_path() 211 if not self._tarball_path: 212 self._create_tarball() 213 214 def _create_tarball_dir(self) -> None: 215 os.makedirs(self._tarball_dir, exist_ok=True) 216 217 def _check_tarball_path(self) -> Path | None: 218 if self._tarball_name in os.listdir(self._tarball_dir): 219 return Path(self._tarball_dir, self._tarball_name) 220 return None 221 222 def _create_tarball(self) -> None: 223 self._tarball_path = Path(self._tarball_dir, self._tarball_name) 224 225 atexit.register(self._delete_tarball) 226 227 result = subprocess.run( 228 'git -C "$(git rev-parse --show-toplevel)" archive ' 229 f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | ' 230 f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}", 231 shell=True, 232 text=True, 233 capture_output=True, 234 ) 235 236 if result.returncode != 0: 237 raise SubprocessError( 238 f"Git archive creation failed with exit code {result.returncode}.\n" 239 f"Command: {result.args}\n" 240 f"Stdout: {result.stdout}\n" 241 f"Stderr: {result.stderr}" 242 ) 243 244 atexit.unregister(self._delete_tarball) 245 246 def _delete_tarball(self) -> None: 247 if self._tarball_path and os.path.exists(self._tarball_path): 248 os.remove(self._tarball_path) 249 250 def __fspath__(self) -> str: 251 """The os.PathLike protocol implementation.""" 252 return str(self._tarball_path) 253 254 255class PacketProtocols(Flag): 256 """Flag specifying which protocols to use for packet generation.""" 257 258 #: 259 IP = 1 260 #: 261 TCP = 2 | IP 262 #: 263 UDP = 4 | IP 264 #: 265 ALL = TCP | UDP 266 267 268def generate_random_packets( 269 number_of: int, 270 payload_size: int = 1500, 271 protocols: PacketProtocols = PacketProtocols.ALL, 272 ports_range: range = range(1024, 49152), 273 mtu: int = 1500, 274) -> list[Packet]: 275 """Generate a number of random packets. 276 277 The payload of the packets will consist of random bytes. If `payload_size` is too big, then the 278 maximum payload size allowed for the specific packet type is used. The size is calculated based 279 on the specified `mtu`, therefore it is essential that `mtu` is set correctly to match the MTU 280 of the port that will send out the generated packets. 281 282 If `protocols` has any L4 protocol enabled then all the packets are generated with any of 283 the specified L4 protocols chosen at random. If only :attr:`~PacketProtocols.IP` is set, then 284 only L3 packets are generated. 285 286 If L4 packets will be generated, then the TCP/UDP ports to be used will be chosen at random from 287 `ports_range`. 288 289 Args: 290 number_of: The number of packets to generate. 291 payload_size: The packet payload size to generate, capped based on `mtu`. 292 protocols: The protocols to use for the generated packets. 293 ports_range: The range of L4 port numbers to use. Used only if `protocols` has L4 protocols. 294 mtu: The MTU of the NIC port that will send out the generated packets. 295 296 Raises: 297 InternalError: If the `payload_size` is invalid. 298 299 Returns: 300 A list containing the randomly generated packets. 301 """ 302 if payload_size < 0: 303 raise InternalError(f"An invalid payload_size of {payload_size} was given.") 304 305 l4_factories = [] 306 if protocols & PacketProtocols.TCP: 307 l4_factories.append(TCP) 308 if protocols & PacketProtocols.UDP: 309 l4_factories.append(UDP) 310 311 def _make_packet() -> Packet: 312 packet = Ether() 313 314 if protocols & PacketProtocols.IP: 315 packet /= IP() 316 317 if len(l4_factories) > 0: 318 src_port, dst_port = random.choices(ports_range, k=2) 319 packet /= random.choice(l4_factories)(sport=src_port, dport=dst_port) 320 321 max_payload_size = mtu - len(packet) 322 usable_payload_size = payload_size if payload_size < max_payload_size else max_payload_size 323 return packet / random.randbytes(usable_payload_size) 324 325 return [_make_packet() for _ in range(number_of)] 326 327 328class MultiInheritanceBaseClass: 329 """A base class for classes utilizing multiple inheritance. 330 331 This class enables it's subclasses to support both single and multiple inheritance by acting as 332 a stopping point in the tree of calls to the constructors of superclasses. This class is able 333 to exist at the end of the Method Resolution Order (MRO) so that subclasses can call 334 :meth:`super.__init__` without repercussion. 335 """ 336 337 def __init__(self, *args, **kwargs) -> None: 338 """Call the init method of :class:`object`.""" 339 super().__init__() 340