xref: /dpdk/dts/framework/utils.py (revision 5d52418fa4b9a7f28eaedc1d88ec5cf330381c0e)
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
6import atexit
7import json
8import os
9import subprocess
10import sys
11from enum import Enum
12from pathlib import Path
13from subprocess import SubprocessError
14
15from scapy.packet import Packet  # type: ignore[import]
16
17from .exception import ConfigurationError
18
19
20class StrEnum(Enum):
21    @staticmethod
22    def _generate_next_value_(
23        name: str, start: int, count: int, last_values: object
24    ) -> str:
25        return name
26
27    def __str__(self) -> str:
28        return self.name
29
30
31REGEX_FOR_PCI_ADDRESS = "/[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}.[0-9]{1}/"
32
33
34def check_dts_python_version() -> None:
35    if sys.version_info.major < 3 or (
36        sys.version_info.major == 3 and sys.version_info.minor < 10
37    ):
38        print(
39            RED(
40                (
41                    "WARNING: DTS execution node's python version is lower than"
42                    "python 3.10, is deprecated and will not work in future releases."
43                )
44            ),
45            file=sys.stderr,
46        )
47        print(RED("Please use Python >= 3.10 instead"), file=sys.stderr)
48
49
50def expand_range(range_str: str) -> list[int]:
51    """
52    Process range string into a list of integers. There are two possible formats:
53    n - a single integer
54    n-m - a range of integers
55
56    The returned range includes both n and m. Empty string returns an empty list.
57    """
58    expanded_range: list[int] = []
59    if range_str:
60        range_boundaries = range_str.split("-")
61        # will throw an exception when items in range_boundaries can't be converted,
62        # serving as type check
63        expanded_range.extend(
64            range(int(range_boundaries[0]), int(range_boundaries[-1]) + 1)
65        )
66
67    return expanded_range
68
69
70def get_packet_summaries(packets: list[Packet]):
71    if len(packets) == 1:
72        packet_summaries = packets[0].summary()
73    else:
74        packet_summaries = json.dumps(
75            list(map(lambda pkt: pkt.summary(), packets)), indent=4
76        )
77    return f"Packet contents: \n{packet_summaries}"
78
79
80def RED(text: str) -> str:
81    return f"\u001B[31;1m{str(text)}\u001B[0m"
82
83
84class MesonArgs(object):
85    """
86    Aggregate the arguments needed to build DPDK:
87    default_library: Default library type, Meson allows "shared", "static" and "both".
88               Defaults to None, in which case the argument won't be used.
89    Keyword arguments: The arguments found in meson_options.txt in root DPDK directory.
90               Do not use -D with them, for example:
91               meson_args = MesonArgs(enable_kmods=True).
92    """
93
94    _default_library: str
95
96    def __init__(self, default_library: str | None = None, **dpdk_args: str | bool):
97        self._default_library = (
98            f"--default-library={default_library}" if default_library else ""
99        )
100        self._dpdk_args = " ".join(
101            (
102                f"-D{dpdk_arg_name}={dpdk_arg_value}"
103                for dpdk_arg_name, dpdk_arg_value in dpdk_args.items()
104            )
105        )
106
107    def __str__(self) -> str:
108        return " ".join(f"{self._default_library} {self._dpdk_args}".split())
109
110
111class _TarCompressionFormat(StrEnum):
112    """Compression formats that tar can use.
113
114    Enum names are the shell compression commands
115    and Enum values are the associated file extensions.
116    """
117
118    gzip = "gz"
119    compress = "Z"
120    bzip2 = "bz2"
121    lzip = "lz"
122    lzma = "lzma"
123    lzop = "lzo"
124    xz = "xz"
125    zstd = "zst"
126
127
128class DPDKGitTarball(object):
129    """Create a compressed tarball of DPDK from the repository.
130
131    The DPDK version is specified with git object git_ref.
132    The tarball will be compressed with _TarCompressionFormat,
133    which must be supported by the DTS execution environment.
134    The resulting tarball will be put into output_dir.
135
136    The class supports the os.PathLike protocol,
137    which is used to get the Path of the tarball::
138
139        from pathlib import Path
140        tarball = DPDKGitTarball("HEAD", "output")
141        tarball_path = Path(tarball)
142
143    Arguments:
144        git_ref: A git commit ID, tag ID or tree ID.
145        output_dir: The directory where to put the resulting tarball.
146        tar_compression_format: The compression format to use.
147    """
148
149    _git_ref: str
150    _tar_compression_format: _TarCompressionFormat
151    _tarball_dir: Path
152    _tarball_name: str
153    _tarball_path: Path | None
154
155    def __init__(
156        self,
157        git_ref: str,
158        output_dir: str,
159        tar_compression_format: _TarCompressionFormat = _TarCompressionFormat.xz,
160    ):
161        self._git_ref = git_ref
162        self._tar_compression_format = tar_compression_format
163
164        self._tarball_dir = Path(output_dir, "tarball")
165
166        self._get_commit_id()
167        self._create_tarball_dir()
168
169        self._tarball_name = (
170            f"dpdk-tarball-{self._git_ref}.tar.{self._tar_compression_format.value}"
171        )
172        self._tarball_path = self._check_tarball_path()
173        if not self._tarball_path:
174            self._create_tarball()
175
176    def _get_commit_id(self) -> None:
177        result = subprocess.run(
178            ["git", "rev-parse", "--verify", self._git_ref],
179            text=True,
180            capture_output=True,
181        )
182        if result.returncode != 0:
183            raise ConfigurationError(
184                f"{self._git_ref} is neither a path to an existing DPDK "
185                "archive nor a valid git reference.\n"
186                f"Command: {result.args}\n"
187                f"Stdout: {result.stdout}\n"
188                f"Stderr: {result.stderr}"
189            )
190        self._git_ref = result.stdout.strip()
191
192    def _create_tarball_dir(self) -> None:
193        os.makedirs(self._tarball_dir, exist_ok=True)
194
195    def _check_tarball_path(self) -> Path | None:
196        if self._tarball_name in os.listdir(self._tarball_dir):
197            return Path(self._tarball_dir, self._tarball_name)
198        return None
199
200    def _create_tarball(self) -> None:
201        self._tarball_path = Path(self._tarball_dir, self._tarball_name)
202
203        atexit.register(self._delete_tarball)
204
205        result = subprocess.run(
206            'git -C "$(git rev-parse --show-toplevel)" archive '
207            f'{self._git_ref} --prefix="dpdk-tarball-{self._git_ref + os.sep}" | '
208            f"{self._tar_compression_format} > {Path(self._tarball_path.absolute())}",
209            shell=True,
210            text=True,
211            capture_output=True,
212        )
213
214        if result.returncode != 0:
215            raise SubprocessError(
216                f"Git archive creation failed with exit code {result.returncode}.\n"
217                f"Command: {result.args}\n"
218                f"Stdout: {result.stdout}\n"
219                f"Stderr: {result.stderr}"
220            )
221
222        atexit.unregister(self._delete_tarball)
223
224    def _delete_tarball(self) -> None:
225        if self._tarball_path and os.path.exists(self._tarball_path):
226            os.remove(self._tarball_path)
227
228    def __fspath__(self):
229        return str(self._tarball_path)
230