xref: /dpdk/dts/framework/utils.py (revision 80158fd411bb06d1a1c22f56fd387ecc49eaf15d)
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
18*80158fd4STomáš Ďurovecimport fnmatch
19cecfe0aaSJuraj Linkešimport json
20c9d31a7eSJuraj Linkešimport os
21f51557fbSLuca Vizzarroimport random
22c9d31a7eSJuraj Linkešimport subprocess
23*80158fd4STomáš Ďurovecimport tarfile
24f51557fbSLuca Vizzarrofrom enum import Enum, Flag
25c9d31a7eSJuraj Linkešfrom pathlib import Path
26c9d31a7eSJuraj Linkešfrom subprocess import SubprocessError
27*80158fd4STomáš Ďurovecfrom typing import Any, Callable
28c9d31a7eSJuraj Linkeš
29f51557fbSLuca Vizzarrofrom scapy.layers.inet import IP, TCP, UDP, Ether  # type: ignore[import-untyped]
30282688eaSLuca Vizzarrofrom scapy.packet import Packet  # type: ignore[import-untyped]
31cecfe0aaSJuraj Linkeš
32f51557fbSLuca Vizzarrofrom .exception import ConfigurationError, InternalError
33c9d31a7eSJuraj Linkeš
34840b1e01SJuraj LinkešREGEX_FOR_PCI_ADDRESS: str = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"
35a91d5f47SJeremy Spewock_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC: str = r"(?:[\da-fA-F]{2}[:-]){5}[\da-fA-F]{2}"
36a91d5f47SJeremy Spewock_REGEX_FOR_DOT_SEP_MAC: str = r"(?:[\da-fA-F]{4}.){2}[\da-fA-F]{4}"
37a91d5f47SJeremy SpewockREGEX_FOR_MAC_ADDRESS: str = rf"{_REGEX_FOR_COLON_OR_HYPHEN_SEP_MAC}|{_REGEX_FOR_DOT_SEP_MAC}"
3899740300SJeremy SpewockREGEX_FOR_BASE64_ENCODING: str = "[-a-zA-Z0-9+\\/]*={0,3}"
3957c58bf8SJuraj Linkeš
40812c4071SJuraj Linkeš
41c020b7ceSJuraj Linkešdef expand_range(range_str: str) -> list[int]:
426ef07151SJuraj Linkeš    """Process `range_str` into a list of integers.
43c020b7ceSJuraj Linkeš
446ef07151SJuraj Linkeš    There are two possible formats of `range_str`:
456ef07151SJuraj Linkeš
466ef07151SJuraj Linkeš        * ``n`` - a single integer,
476ef07151SJuraj Linkeš        * ``n-m`` - a range of integers.
486ef07151SJuraj Linkeš
496ef07151SJuraj Linkeš    The returned range includes both ``n`` and ``m``. Empty string returns an empty list.
506ef07151SJuraj Linkeš
516ef07151SJuraj Linkeš    Args:
526ef07151SJuraj Linkeš        range_str: The range to expand.
536ef07151SJuraj Linkeš
546ef07151SJuraj Linkeš    Returns:
556ef07151SJuraj Linkeš        All the numbers from the range.
56c020b7ceSJuraj Linkeš    """
57c020b7ceSJuraj Linkeš    expanded_range: list[int] = []
58c020b7ceSJuraj Linkeš    if range_str:
59c020b7ceSJuraj Linkeš        range_boundaries = range_str.split("-")
60c020b7ceSJuraj Linkeš        # will throw an exception when items in range_boundaries can't be converted,
61c020b7ceSJuraj Linkeš        # serving as type check
62517b4b26SJuraj Linkeš        expanded_range.extend(range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1))
63c020b7ceSJuraj Linkeš
64c020b7ceSJuraj Linkeš    return expanded_range
65c020b7ceSJuraj Linkeš
66c020b7ceSJuraj Linkeš
67840b1e01SJuraj Linkešdef get_packet_summaries(packets: list[Packet]) -> str:
686ef07151SJuraj Linkeš    """Format a string summary from `packets`.
696ef07151SJuraj Linkeš
706ef07151SJuraj Linkeš    Args:
716ef07151SJuraj Linkeš        packets: The packets to format.
726ef07151SJuraj Linkeš
736ef07151SJuraj Linkeš    Returns:
746ef07151SJuraj Linkeš        The summary of `packets`.
756ef07151SJuraj Linkeš    """
76cecfe0aaSJuraj Linkeš    if len(packets) == 1:
77cecfe0aaSJuraj Linkeš        packet_summaries = packets[0].summary()
78cecfe0aaSJuraj Linkeš    else:
79517b4b26SJuraj Linkeš        packet_summaries = json.dumps(list(map(lambda pkt: pkt.summary(), packets)), indent=4)
80cecfe0aaSJuraj Linkeš    return f"Packet contents: \n{packet_summaries}"
81cecfe0aaSJuraj Linkeš
82cecfe0aaSJuraj Linkeš
83a23f2245SLuca Vizzarrodef get_commit_id(rev_id: str) -> str:
84a23f2245SLuca Vizzarro    """Given a Git revision ID, return the corresponding commit ID.
85a23f2245SLuca Vizzarro
86a23f2245SLuca Vizzarro    Args:
87a23f2245SLuca Vizzarro        rev_id: The Git revision ID.
88a23f2245SLuca Vizzarro
89a23f2245SLuca Vizzarro    Raises:
90a23f2245SLuca Vizzarro        ConfigurationError: The ``git rev-parse`` command failed, suggesting
91a23f2245SLuca Vizzarro            an invalid or ambiguous revision ID was supplied.
92a23f2245SLuca Vizzarro    """
93a23f2245SLuca Vizzarro    result = subprocess.run(
94a23f2245SLuca Vizzarro        ["git", "rev-parse", "--verify", rev_id],
95a23f2245SLuca Vizzarro        text=True,
96a23f2245SLuca Vizzarro        capture_output=True,
97a23f2245SLuca Vizzarro    )
98a23f2245SLuca Vizzarro    if result.returncode != 0:
99a23f2245SLuca Vizzarro        raise ConfigurationError(
100a23f2245SLuca Vizzarro            f"{rev_id} is not a valid git reference.\n"
101a23f2245SLuca Vizzarro            f"Command: {result.args}\n"
102a23f2245SLuca Vizzarro            f"Stdout: {result.stdout}\n"
103a23f2245SLuca Vizzarro            f"Stderr: {result.stderr}"
104a23f2245SLuca Vizzarro        )
105a23f2245SLuca Vizzarro    return result.stdout.strip()
106a23f2245SLuca Vizzarro
107a23f2245SLuca Vizzarro
108840b1e01SJuraj Linkešclass StrEnum(Enum):
1096ef07151SJuraj Linkeš    """Enum with members stored as strings."""
1106ef07151SJuraj Linkeš
111840b1e01SJuraj Linkeš    @staticmethod
112840b1e01SJuraj Linkeš    def _generate_next_value_(name: str, start: int, count: int, last_values: object) -> str:
113840b1e01SJuraj Linkeš        return name
114840b1e01SJuraj Linkeš
115840b1e01SJuraj Linkeš    def __str__(self) -> str:
1166ef07151SJuraj Linkeš        """The string representation is the name of the member."""
117840b1e01SJuraj Linkeš        return self.name
118680d8a24SJuraj Linkeš
119680d8a24SJuraj Linkeš
1203e967643SJuraj Linkešclass MesonArgs:
1216ef07151SJuraj Linkeš    """Aggregate the arguments needed to build DPDK."""
122680d8a24SJuraj Linkeš
123680d8a24SJuraj Linkeš    _default_library: str
124680d8a24SJuraj Linkeš
125680d8a24SJuraj Linkeš    def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
1266ef07151SJuraj Linkeš        """Initialize the meson arguments.
1276ef07151SJuraj Linkeš
1286ef07151SJuraj Linkeš        Args:
1296ef07151SJuraj Linkeš            default_library: The default library type, Meson supports ``shared``, ``static`` and
1306ef07151SJuraj Linkeš                ``both``. Defaults to :data:`None`, in which case the argument won't be used.
1316ef07151SJuraj Linkeš            dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory.
1326ef07151SJuraj Linkeš                Do not use ``-D`` with them.
1336ef07151SJuraj Linkeš
1346ef07151SJuraj Linkeš        Example:
1356ef07151SJuraj Linkeš            ::
1366ef07151SJuraj Linkeš
1376ef07151SJuraj Linkeš                meson_args = MesonArgs(enable_kmods=True).
1386ef07151SJuraj Linkeš        """
139517b4b26SJuraj Linkeš        self._default_library = f"--default-library={default_library}" if default_library else ""
140680d8a24SJuraj Linkeš        self._dpdk_args = " ".join(
141680d8a24SJuraj Linkeš            (
142680d8a24SJuraj Linkeš                f"-D{dpdk_arg_name}={dpdk_arg_value}"
143680d8a24SJuraj Linkeš                for dpdk_arg_name, dpdk_arg_value in dpdk_args.items()
144680d8a24SJuraj Linkeš            )
145680d8a24SJuraj Linkeš        )
146680d8a24SJuraj Linkeš
147680d8a24SJuraj Linkeš    def __str__(self) -> str:
1486ef07151SJuraj Linkeš        """The actual args."""
149680d8a24SJuraj Linkeš        return " ".join(f"{self._default_library} {self._dpdk_args}".split())
150c9d31a7eSJuraj Linkeš
151c9d31a7eSJuraj Linkeš
152*80158fd4STomáš Ďurovecclass TarCompressionFormat(StrEnum):
153c9d31a7eSJuraj Linkeš    """Compression formats that tar can use.
154c9d31a7eSJuraj Linkeš
155c9d31a7eSJuraj Linkeš    Enum names are the shell compression commands
156c9d31a7eSJuraj Linkeš    and Enum values are the associated file extensions.
157*80158fd4STomáš Ďurovec
158*80158fd4STomáš Ďurovec    The 'none' member represents no compression, only archiving with tar.
159*80158fd4STomáš Ďurovec    Its value is set to 'tar' to indicate that the file is an uncompressed tar archive.
160c9d31a7eSJuraj Linkeš    """
161c9d31a7eSJuraj Linkeš
162*80158fd4STomáš Ďurovec    none = "tar"
163c9d31a7eSJuraj Linkeš    gzip = "gz"
164c9d31a7eSJuraj Linkeš    compress = "Z"
165c9d31a7eSJuraj Linkeš    bzip2 = "bz2"
166c9d31a7eSJuraj Linkeš    lzip = "lz"
167c9d31a7eSJuraj Linkeš    lzma = "lzma"
168c9d31a7eSJuraj Linkeš    lzop = "lzo"
169c9d31a7eSJuraj Linkeš    xz = "xz"
170c9d31a7eSJuraj Linkeš    zstd = "zst"
171c9d31a7eSJuraj Linkeš
172*80158fd4STomáš Ďurovec    @property
173*80158fd4STomáš Ďurovec    def extension(self):
174*80158fd4STomáš Ďurovec        """Return the extension associated with the compression format.
175*80158fd4STomáš Ďurovec
176*80158fd4STomáš Ďurovec        If the compression format is 'none', the extension will be in the format 'tar'.
177*80158fd4STomáš Ďurovec        For other compression formats, the extension will be in the format
178*80158fd4STomáš Ďurovec        'tar.{compression format}'.
179*80158fd4STomáš Ďurovec        """
180*80158fd4STomáš Ďurovec        return f"{self.value}" if self == self.none else f"{self.none.value}.{self.value}"
181*80158fd4STomáš Ďurovec
182c9d31a7eSJuraj Linkeš
1833e967643SJuraj Linkešclass DPDKGitTarball:
1846ef07151SJuraj Linkeš    """Compressed tarball of DPDK from the repository.
185c9d31a7eSJuraj Linkeš
1866ef07151SJuraj Linkeš    The class supports the :class:`os.PathLike` protocol,
187c9d31a7eSJuraj Linkeš    which is used to get the Path of the tarball::
188c9d31a7eSJuraj Linkeš
189c9d31a7eSJuraj Linkeš        from pathlib import Path
190c9d31a7eSJuraj Linkeš        tarball = DPDKGitTarball("HEAD", "output")
191c9d31a7eSJuraj Linkeš        tarball_path = Path(tarball)
192c9d31a7eSJuraj Linkeš    """
193c9d31a7eSJuraj Linkeš
194c9d31a7eSJuraj Linkeš    _git_ref: str
195*80158fd4STomáš Ďurovec    _tar_compression_format: TarCompressionFormat
196c9d31a7eSJuraj Linkeš    _tarball_dir: Path
197c9d31a7eSJuraj Linkeš    _tarball_name: str
198c9d31a7eSJuraj Linkeš    _tarball_path: Path | None
199c9d31a7eSJuraj Linkeš
200c9d31a7eSJuraj Linkeš    def __init__(
201c9d31a7eSJuraj Linkeš        self,
202c9d31a7eSJuraj Linkeš        git_ref: str,
203c9d31a7eSJuraj Linkeš        output_dir: str,
204*80158fd4STomáš Ďurovec        tar_compression_format: TarCompressionFormat = TarCompressionFormat.xz,
205c9d31a7eSJuraj Linkeš    ):
2066ef07151SJuraj Linkeš        """Create the tarball during initialization.
2076ef07151SJuraj Linkeš
2086ef07151SJuraj Linkeš        The DPDK version is specified with `git_ref`. The tarball will be compressed with
2096ef07151SJuraj Linkeš        `tar_compression_format`, which must be supported by the DTS execution environment.
2106ef07151SJuraj Linkeš        The resulting tarball will be put into `output_dir`.
2116ef07151SJuraj Linkeš
2126ef07151SJuraj Linkeš        Args:
2136ef07151SJuraj Linkeš            git_ref: A git commit ID, tag ID or tree ID.
2146ef07151SJuraj Linkeš            output_dir: The directory where to put the resulting tarball.
2156ef07151SJuraj Linkeš            tar_compression_format: The compression format to use.
2166ef07151SJuraj Linkeš        """
217c9d31a7eSJuraj Linkeš        self._git_ref = git_ref
218c9d31a7eSJuraj Linkeš        self._tar_compression_format = tar_compression_format
219c9d31a7eSJuraj Linkeš
220c9d31a7eSJuraj Linkeš        self._tarball_dir = Path(output_dir, "tarball")
221c9d31a7eSJuraj Linkeš
222c9d31a7eSJuraj Linkeš        self._create_tarball_dir()
223c9d31a7eSJuraj Linkeš
224c9d31a7eSJuraj Linkeš        self._tarball_name = (
225*80158fd4STomáš Ďurovec            f"dpdk-tarball-{self._git_ref}.{self._tar_compression_format.extension}"
226c9d31a7eSJuraj Linkeš        )
227c9d31a7eSJuraj Linkeš        self._tarball_path = self._check_tarball_path()
228c9d31a7eSJuraj Linkeš        if not self._tarball_path:
229c9d31a7eSJuraj Linkeš            self._create_tarball()
230c9d31a7eSJuraj Linkeš
231c9d31a7eSJuraj Linkeš    def _create_tarball_dir(self) -> None:
232c9d31a7eSJuraj Linkeš        os.makedirs(self._tarball_dir, exist_ok=True)
233c9d31a7eSJuraj Linkeš
234c9d31a7eSJuraj Linkeš    def _check_tarball_path(self) -> Path | None:
235c9d31a7eSJuraj Linkeš        if self._tarball_name in os.listdir(self._tarball_dir):
236c9d31a7eSJuraj Linkeš            return Path(self._tarball_dir, self._tarball_name)
237c9d31a7eSJuraj Linkeš        return None
238c9d31a7eSJuraj Linkeš
239c9d31a7eSJuraj Linkeš    def _create_tarball(self) -> None:
240c9d31a7eSJuraj Linkeš        self._tarball_path = Path(self._tarball_dir, self._tarball_name)
241c9d31a7eSJuraj Linkeš
242c9d31a7eSJuraj Linkeš        atexit.register(self._delete_tarball)
243c9d31a7eSJuraj Linkeš
244c9d31a7eSJuraj Linkeš        result = subprocess.run(
245c9d31a7eSJuraj Linkeš            'git -C "$(git rev-parse --show-toplevel)" archive '
246c9d31a7eSJuraj Linkeš            f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | '
247c9d31a7eSJuraj Linkeš            f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}",
248c9d31a7eSJuraj Linkeš            shell=True,
249c9d31a7eSJuraj Linkeš            text=True,
250c9d31a7eSJuraj Linkeš            capture_output=True,
251c9d31a7eSJuraj Linkeš        )
252c9d31a7eSJuraj Linkeš
253c9d31a7eSJuraj Linkeš        if result.returncode != 0:
254c9d31a7eSJuraj Linkeš            raise SubprocessError(
255c9d31a7eSJuraj Linkeš                f"Git archive creation failed with exit code {result.returncode}.\n"
256c9d31a7eSJuraj Linkeš                f"Command: {result.args}\n"
257c9d31a7eSJuraj Linkeš                f"Stdout: {result.stdout}\n"
258c9d31a7eSJuraj Linkeš                f"Stderr: {result.stderr}"
259c9d31a7eSJuraj Linkeš            )
260c9d31a7eSJuraj Linkeš
261c9d31a7eSJuraj Linkeš        atexit.unregister(self._delete_tarball)
262c9d31a7eSJuraj Linkeš
263c9d31a7eSJuraj Linkeš    def _delete_tarball(self) -> None:
264c9d31a7eSJuraj Linkeš        if self._tarball_path and os.path.exists(self._tarball_path):
265c9d31a7eSJuraj Linkeš            os.remove(self._tarball_path)
266c9d31a7eSJuraj Linkeš
267840b1e01SJuraj Linkeš    def __fspath__(self) -> str:
2686ef07151SJuraj Linkeš        """The os.PathLike protocol implementation."""
269c9d31a7eSJuraj Linkeš        return str(self._tarball_path)
270f51557fbSLuca Vizzarro
271f51557fbSLuca Vizzarro
272*80158fd4STomáš Ďurovecdef convert_to_list_of_string(value: Any | list[Any]) -> list[str]:
273*80158fd4STomáš Ďurovec    """Convert the input to the list of strings."""
274*80158fd4STomáš Ďurovec    return list(map(str, value) if isinstance(value, list) else str(value))
275*80158fd4STomáš Ďurovec
276*80158fd4STomáš Ďurovec
277*80158fd4STomáš Ďurovecdef create_tarball(
278*80158fd4STomáš Ďurovec    dir_path: Path,
279*80158fd4STomáš Ďurovec    compress_format: TarCompressionFormat = TarCompressionFormat.none,
280*80158fd4STomáš Ďurovec    exclude: Any | list[Any] | None = None,
281*80158fd4STomáš Ďurovec) -> Path:
282*80158fd4STomáš Ďurovec    """Create a tarball from the contents of the specified directory.
283*80158fd4STomáš Ďurovec
284*80158fd4STomáš Ďurovec    This method creates a tarball containing all files and directories within `dir_path`.
285*80158fd4STomáš Ďurovec    The tarball will be saved in the directory of `dir_path` and will be named based on `dir_path`.
286*80158fd4STomáš Ďurovec
287*80158fd4STomáš Ďurovec    Args:
288*80158fd4STomáš Ďurovec        dir_path: The directory path.
289*80158fd4STomáš Ďurovec        compress_format: The compression format to use. Defaults to no compression.
290*80158fd4STomáš Ďurovec        exclude: Patterns for files or directories to exclude from the tarball.
291*80158fd4STomáš Ďurovec                These patterns are used with `fnmatch.fnmatch` to filter out files.
292*80158fd4STomáš Ďurovec
293*80158fd4STomáš Ďurovec    Returns:
294*80158fd4STomáš Ďurovec        The path to the created tarball.
295*80158fd4STomáš Ďurovec    """
296*80158fd4STomáš Ďurovec
297*80158fd4STomáš Ďurovec    def create_filter_function(exclude_patterns: str | list[str] | None) -> Callable | None:
298*80158fd4STomáš Ďurovec        """Create a filter function based on the provided exclude patterns.
299*80158fd4STomáš Ďurovec
300*80158fd4STomáš Ďurovec        Args:
301*80158fd4STomáš Ďurovec            exclude_patterns: Patterns for files or directories to exclude from the tarball.
302*80158fd4STomáš Ďurovec                These patterns are used with `fnmatch.fnmatch` to filter out files.
303*80158fd4STomáš Ďurovec
304*80158fd4STomáš Ďurovec        Returns:
305*80158fd4STomáš Ďurovec            The filter function that excludes files based on the patterns.
306*80158fd4STomáš Ďurovec        """
307*80158fd4STomáš Ďurovec        if exclude_patterns:
308*80158fd4STomáš Ďurovec            exclude_patterns = convert_to_list_of_string(exclude_patterns)
309*80158fd4STomáš Ďurovec
310*80158fd4STomáš Ďurovec            def filter_func(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:
311*80158fd4STomáš Ďurovec                file_name = os.path.basename(tarinfo.name)
312*80158fd4STomáš Ďurovec                if any(fnmatch.fnmatch(file_name, pattern) for pattern in exclude_patterns):
313*80158fd4STomáš Ďurovec                    return None
314*80158fd4STomáš Ďurovec                return tarinfo
315*80158fd4STomáš Ďurovec
316*80158fd4STomáš Ďurovec            return filter_func
317*80158fd4STomáš Ďurovec        return None
318*80158fd4STomáš Ďurovec
319*80158fd4STomáš Ďurovec    target_tarball_path = dir_path.with_suffix(f".{compress_format.extension}")
320*80158fd4STomáš Ďurovec    with tarfile.open(target_tarball_path, f"w:{compress_format.value}") as tar:
321*80158fd4STomáš Ďurovec        tar.add(dir_path, arcname=dir_path.name, filter=create_filter_function(exclude))
322*80158fd4STomáš Ďurovec
323*80158fd4STomáš Ďurovec    return target_tarball_path
324*80158fd4STomáš Ďurovec
325*80158fd4STomáš Ďurovec
326*80158fd4STomáš Ďurovecdef extract_tarball(tar_path: str | Path):
327*80158fd4STomáš Ďurovec    """Extract the contents of a tarball.
328*80158fd4STomáš Ďurovec
329*80158fd4STomáš Ďurovec    The tarball will be extracted in the same path as `tar_path` parent path.
330*80158fd4STomáš Ďurovec
331*80158fd4STomáš Ďurovec    Args:
332*80158fd4STomáš Ďurovec        tar_path: The path to the tarball file to extract.
333*80158fd4STomáš Ďurovec    """
334*80158fd4STomáš Ďurovec    with tarfile.open(tar_path, "r") as tar:
335*80158fd4STomáš Ďurovec        tar.extractall(path=Path(tar_path).parent)
336*80158fd4STomáš Ďurovec
337*80158fd4STomáš Ďurovec
338f51557fbSLuca Vizzarroclass PacketProtocols(Flag):
339f51557fbSLuca Vizzarro    """Flag specifying which protocols to use for packet generation."""
340f51557fbSLuca Vizzarro
341f51557fbSLuca Vizzarro    #:
342f51557fbSLuca Vizzarro    IP = 1
343f51557fbSLuca Vizzarro    #:
344f51557fbSLuca Vizzarro    TCP = 2 | IP
345f51557fbSLuca Vizzarro    #:
346f51557fbSLuca Vizzarro    UDP = 4 | IP
347f51557fbSLuca Vizzarro    #:
348f51557fbSLuca Vizzarro    ALL = TCP | UDP
349f51557fbSLuca Vizzarro
350f51557fbSLuca Vizzarro
351f51557fbSLuca Vizzarrodef generate_random_packets(
352f51557fbSLuca Vizzarro    number_of: int,
353f51557fbSLuca Vizzarro    payload_size: int = 1500,
354f51557fbSLuca Vizzarro    protocols: PacketProtocols = PacketProtocols.ALL,
355f51557fbSLuca Vizzarro    ports_range: range = range(1024, 49152),
356f51557fbSLuca Vizzarro    mtu: int = 1500,
357f51557fbSLuca Vizzarro) -> list[Packet]:
358f51557fbSLuca Vizzarro    """Generate a number of random packets.
359f51557fbSLuca Vizzarro
360f51557fbSLuca Vizzarro    The payload of the packets will consist of random bytes. If `payload_size` is too big, then the
361f51557fbSLuca Vizzarro    maximum payload size allowed for the specific packet type is used. The size is calculated based
362f51557fbSLuca Vizzarro    on the specified `mtu`, therefore it is essential that `mtu` is set correctly to match the MTU
363f51557fbSLuca Vizzarro    of the port that will send out the generated packets.
364f51557fbSLuca Vizzarro
365f51557fbSLuca Vizzarro    If `protocols` has any L4 protocol enabled then all the packets are generated with any of
366f51557fbSLuca Vizzarro    the specified L4 protocols chosen at random. If only :attr:`~PacketProtocols.IP` is set, then
367f51557fbSLuca Vizzarro    only L3 packets are generated.
368f51557fbSLuca Vizzarro
369f51557fbSLuca Vizzarro    If L4 packets will be generated, then the TCP/UDP ports to be used will be chosen at random from
370f51557fbSLuca Vizzarro    `ports_range`.
371f51557fbSLuca Vizzarro
372f51557fbSLuca Vizzarro    Args:
373f51557fbSLuca Vizzarro        number_of: The number of packets to generate.
374f51557fbSLuca Vizzarro        payload_size: The packet payload size to generate, capped based on `mtu`.
375f51557fbSLuca Vizzarro        protocols: The protocols to use for the generated packets.
376f51557fbSLuca Vizzarro        ports_range: The range of L4 port numbers to use. Used only if `protocols` has L4 protocols.
377f51557fbSLuca Vizzarro        mtu: The MTU of the NIC port that will send out the generated packets.
378f51557fbSLuca Vizzarro
379f51557fbSLuca Vizzarro    Raises:
380f51557fbSLuca Vizzarro        InternalError: If the `payload_size` is invalid.
381f51557fbSLuca Vizzarro
382f51557fbSLuca Vizzarro    Returns:
383f51557fbSLuca Vizzarro        A list containing the randomly generated packets.
384f51557fbSLuca Vizzarro    """
385f51557fbSLuca Vizzarro    if payload_size < 0:
386f51557fbSLuca Vizzarro        raise InternalError(f"An invalid payload_size of {payload_size} was given.")
387f51557fbSLuca Vizzarro
388f51557fbSLuca Vizzarro    l4_factories = []
389f51557fbSLuca Vizzarro    if protocols & PacketProtocols.TCP:
390f51557fbSLuca Vizzarro        l4_factories.append(TCP)
391f51557fbSLuca Vizzarro    if protocols & PacketProtocols.UDP:
392f51557fbSLuca Vizzarro        l4_factories.append(UDP)
393f51557fbSLuca Vizzarro
394f51557fbSLuca Vizzarro    def _make_packet() -> Packet:
395f51557fbSLuca Vizzarro        packet = Ether()
396f51557fbSLuca Vizzarro
397f51557fbSLuca Vizzarro        if protocols & PacketProtocols.IP:
398f51557fbSLuca Vizzarro            packet /= IP()
399f51557fbSLuca Vizzarro
400f51557fbSLuca Vizzarro        if len(l4_factories) > 0:
401f51557fbSLuca Vizzarro            src_port, dst_port = random.choices(ports_range, k=2)
402f51557fbSLuca Vizzarro            packet /= random.choice(l4_factories)(sport=src_port, dport=dst_port)
403f51557fbSLuca Vizzarro
404f51557fbSLuca Vizzarro        max_payload_size = mtu - len(packet)
405f51557fbSLuca Vizzarro        usable_payload_size = payload_size if payload_size < max_payload_size else max_payload_size
406f51557fbSLuca Vizzarro        return packet / random.randbytes(usable_payload_size)
407f51557fbSLuca Vizzarro
408f51557fbSLuca Vizzarro    return [_make_packet() for _ in range(number_of)]
40999740300SJeremy Spewock
41099740300SJeremy Spewock
41199740300SJeremy Spewockclass MultiInheritanceBaseClass:
41299740300SJeremy Spewock    """A base class for classes utilizing multiple inheritance.
41399740300SJeremy Spewock
41499740300SJeremy Spewock    This class enables it's subclasses to support both single and multiple inheritance by acting as
41599740300SJeremy Spewock    a stopping point in the tree of calls to the constructors of superclasses. This class is able
41699740300SJeremy Spewock    to exist at the end of the Method Resolution Order (MRO) so that subclasses can call
41799740300SJeremy Spewock    :meth:`super.__init__` without repercussion.
41899740300SJeremy Spewock    """
41999740300SJeremy Spewock
42099740300SJeremy Spewock    def __init__(self, *args, **kwargs) -> None:
42199740300SJeremy Spewock        """Call the init method of :class:`object`."""
42299740300SJeremy Spewock        super().__init__()
423