xref: /dpdk/dts/framework/utils.py (revision a91d5f4748c6feeff571439c5f5c26c4c92edc9a)
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