xref: /dpdk/usertools/dpdk-pmdinfo.py (revision 0d7dad082ab3d8d74a26cc5feaba4117b0025c58)
13f6f8362SLouise Kilheeney#!/usr/bin/env python3
20a56e151SHemant Agrawal# SPDX-License-Identifier: BSD-3-Clause
30a56e151SHemant Agrawal# Copyright(c) 2016  Neil Horman <nhorman@tuxdriver.com>
40ce3cf4aSRobin Jarry# Copyright(c) 2022  Robin Jarry
50ce3cf4aSRobin Jarry# pylint: disable=invalid-name
6c6dab2a8SThomas Monjalon
70ce3cf4aSRobin Jarryr"""
80ce3cf4aSRobin JarryUtility to dump PMD_INFO_STRING support from DPDK binaries.
90ce3cf4aSRobin Jarry
100ce3cf4aSRobin JarryThis script prints JSON output to be interpreted by other tools. Here are some
110ce3cf4aSRobin Jarryexamples with jq:
120ce3cf4aSRobin Jarry
130ce3cf4aSRobin JarryGet the complete info for a given driver:
140ce3cf4aSRobin Jarry
150ce3cf4aSRobin Jarry  %(prog)s dpdk-testpmd | \
160ce3cf4aSRobin Jarry  jq '.[] | select(.name == "cnxk_nix_inl")'
170ce3cf4aSRobin Jarry
180ce3cf4aSRobin JarryGet only the required kernel modules for a given driver:
190ce3cf4aSRobin Jarry
200ce3cf4aSRobin Jarry  %(prog)s dpdk-testpmd | \
210ce3cf4aSRobin Jarry  jq '.[] | select(.name == "net_i40e").kmod'
220ce3cf4aSRobin Jarry
230ce3cf4aSRobin JarryGet only the required kernel modules for a given device:
240ce3cf4aSRobin Jarry
250ce3cf4aSRobin Jarry  %(prog)s dpdk-testpmd | \
26*e0a87c5fSRobin Jarry  jq '.[] | select(.pci_ids[]? | .vendor == "15b3" and .device == "1013").kmod'
270ce3cf4aSRobin Jarry"""
280ce3cf4aSRobin Jarry
2981255f27SStephen Hemmingerimport argparse
300ce3cf4aSRobin Jarryimport json
310ce3cf4aSRobin Jarryimport logging
320ce3cf4aSRobin Jarryimport os
330ce3cf4aSRobin Jarryimport re
340ce3cf4aSRobin Jarryimport string
350ce3cf4aSRobin Jarryimport subprocess
360ce3cf4aSRobin Jarryimport sys
370ce3cf4aSRobin Jarryfrom pathlib import Path
380ce3cf4aSRobin Jarryfrom typing import Iterable, Iterator, List, Union
390ce3cf4aSRobin Jarry
400ce3cf4aSRobin Jarryimport elftools
410ce3cf4aSRobin Jarryfrom elftools.elf.elffile import ELFError, ELFFile
4281255f27SStephen Hemminger
43c6dab2a8SThomas Monjalon
440ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
450ce3cf4aSRobin Jarrydef main() -> int:  # pylint: disable=missing-docstring
460ce3cf4aSRobin Jarry    try:
470ce3cf4aSRobin Jarry        args = parse_args()
480ce3cf4aSRobin Jarry        logging.basicConfig(
490ce3cf4aSRobin Jarry            stream=sys.stderr,
500ce3cf4aSRobin Jarry            format="%(levelname)s: %(message)s",
510ce3cf4aSRobin Jarry            level={
520ce3cf4aSRobin Jarry                0: logging.ERROR,
530ce3cf4aSRobin Jarry                1: logging.WARNING,
540ce3cf4aSRobin Jarry            }.get(args.verbose, logging.DEBUG),
550ce3cf4aSRobin Jarry        )
560ce3cf4aSRobin Jarry        info = parse_pmdinfo(args.elf_files, args.search_plugins)
570ce3cf4aSRobin Jarry        print(json.dumps(info, indent=2))
580ce3cf4aSRobin Jarry    except BrokenPipeError:
59c6dab2a8SThomas Monjalon        pass
600ce3cf4aSRobin Jarry    except KeyboardInterrupt:
610ce3cf4aSRobin Jarry        return 1
620ce3cf4aSRobin Jarry    except Exception as e:  # pylint: disable=broad-except
630ce3cf4aSRobin Jarry        logging.error("%s", e)
640ce3cf4aSRobin Jarry        return 1
650ce3cf4aSRobin Jarry    return 0
66c6dab2a8SThomas Monjalon
67c6dab2a8SThomas Monjalon
680ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
690ce3cf4aSRobin Jarrydef parse_args() -> argparse.Namespace:
70c6dab2a8SThomas Monjalon    """
710ce3cf4aSRobin Jarry    Parse command line arguments.
72c6dab2a8SThomas Monjalon    """
7381255f27SStephen Hemminger    parser = argparse.ArgumentParser(
740ce3cf4aSRobin Jarry        description=__doc__,
750ce3cf4aSRobin Jarry        formatter_class=argparse.RawDescriptionHelpFormatter,
760ce3cf4aSRobin Jarry    )
770ce3cf4aSRobin Jarry    parser.add_argument(
780ce3cf4aSRobin Jarry        "-p",
790ce3cf4aSRobin Jarry        "--search-plugins",
800ce3cf4aSRobin Jarry        action="store_true",
810ce3cf4aSRobin Jarry        help="""
820ce3cf4aSRobin Jarry        In addition of ELF_FILEs and their linked dynamic libraries, also scan
830ce3cf4aSRobin Jarry        the DPDK plugins path.
840ce3cf4aSRobin Jarry        """,
850ce3cf4aSRobin Jarry    )
860ce3cf4aSRobin Jarry    parser.add_argument(
870ce3cf4aSRobin Jarry        "-v",
880ce3cf4aSRobin Jarry        "--verbose",
890ce3cf4aSRobin Jarry        action="count",
900ce3cf4aSRobin Jarry        default=0,
910ce3cf4aSRobin Jarry        help="""
920ce3cf4aSRobin Jarry        Display warnings due to linked libraries not found or ELF/JSON parsing
930ce3cf4aSRobin Jarry        errors in these libraries. Use twice to show debug messages.
940ce3cf4aSRobin Jarry        """,
950ce3cf4aSRobin Jarry    )
960ce3cf4aSRobin Jarry    parser.add_argument(
970ce3cf4aSRobin Jarry        "elf_files",
980ce3cf4aSRobin Jarry        metavar="ELF_FILE",
990ce3cf4aSRobin Jarry        nargs="+",
1000ce3cf4aSRobin Jarry        type=existing_file,
1010ce3cf4aSRobin Jarry        help="""
1020ce3cf4aSRobin Jarry        DPDK application binary or dynamic library.
1030ce3cf4aSRobin Jarry        """,
1040ce3cf4aSRobin Jarry    )
1050ce3cf4aSRobin Jarry    return parser.parse_args()
106c6dab2a8SThomas Monjalon
107c6dab2a8SThomas Monjalon
1080ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
1090ce3cf4aSRobin Jarrydef parse_pmdinfo(paths: Iterable[Path], search_plugins: bool) -> List[dict]:
1100ce3cf4aSRobin Jarry    """
1110ce3cf4aSRobin Jarry    Extract DPDK PMD info JSON strings from an ELF file.
11281255f27SStephen Hemminger
1130ce3cf4aSRobin Jarry    :returns:
1140ce3cf4aSRobin Jarry        A list of DPDK drivers info dictionaries.
1150ce3cf4aSRobin Jarry    """
1160ce3cf4aSRobin Jarry    binaries = set(paths)
1170ce3cf4aSRobin Jarry    for p in paths:
1180ce3cf4aSRobin Jarry        binaries.update(get_needed_libs(p))
1190ce3cf4aSRobin Jarry    if search_plugins:
1200ce3cf4aSRobin Jarry        # cast to list to avoid errors with update while iterating
1210ce3cf4aSRobin Jarry        binaries.update(list(get_plugin_libs(binaries)))
122c6dab2a8SThomas Monjalon
1230ce3cf4aSRobin Jarry    drivers = []
124c6dab2a8SThomas Monjalon
1250ce3cf4aSRobin Jarry    for b in binaries:
1260ce3cf4aSRobin Jarry        logging.debug("analyzing %s", b)
127c6dab2a8SThomas Monjalon        try:
1280ce3cf4aSRobin Jarry            for s in get_elf_strings(b, ".rodata", "PMD_INFO_STRING="):
1290ce3cf4aSRobin Jarry                try:
1300ce3cf4aSRobin Jarry                    info = json.loads(s)
1310ce3cf4aSRobin Jarry                    scrub_pci_ids(info)
1320ce3cf4aSRobin Jarry                    drivers.append(info)
1330ce3cf4aSRobin Jarry                except ValueError as e:
1340ce3cf4aSRobin Jarry                    # invalid JSON, should never happen
1350ce3cf4aSRobin Jarry                    logging.warning("%s: %s", b, e)
1360ce3cf4aSRobin Jarry        except ELFError as e:
1370ce3cf4aSRobin Jarry            # only happens for discovered plugins that are not ELF
1380ce3cf4aSRobin Jarry            logging.debug("%s: cannot parse ELF: %s", b, e)
139c6dab2a8SThomas Monjalon
1400ce3cf4aSRobin Jarry    return drivers
141c6dab2a8SThomas Monjalon
142c6dab2a8SThomas Monjalon
1430ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
1440ce3cf4aSRobin JarryPCI_FIELDS = ("vendor", "device", "subsystem_vendor", "subsystem_device")
1450ce3cf4aSRobin Jarry
1460ce3cf4aSRobin Jarry
1470ce3cf4aSRobin Jarrydef scrub_pci_ids(info: dict):
1480ce3cf4aSRobin Jarry    """
1490ce3cf4aSRobin Jarry    Convert numerical ids to hex strings.
1500ce3cf4aSRobin Jarry    Strip empty pci_ids lists.
1510ce3cf4aSRobin Jarry    Strip wildcard 0xFFFF ids.
1520ce3cf4aSRobin Jarry    """
1530ce3cf4aSRobin Jarry    pci_ids = []
1540ce3cf4aSRobin Jarry    for pci_fields in info.pop("pci_ids"):
1550ce3cf4aSRobin Jarry        pci = {}
1560ce3cf4aSRobin Jarry        for name, value in zip(PCI_FIELDS, pci_fields):
1570ce3cf4aSRobin Jarry            if value != 0xFFFF:
1580ce3cf4aSRobin Jarry                pci[name] = f"{value:04x}"
1590ce3cf4aSRobin Jarry        if pci:
1600ce3cf4aSRobin Jarry            pci_ids.append(pci)
1610ce3cf4aSRobin Jarry    if pci_ids:
1620ce3cf4aSRobin Jarry        info["pci_ids"] = pci_ids
1630ce3cf4aSRobin Jarry
1640ce3cf4aSRobin Jarry
1650ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
1660ce3cf4aSRobin Jarrydef get_plugin_libs(binaries: Iterable[Path]) -> Iterator[Path]:
1670ce3cf4aSRobin Jarry    """
1680ce3cf4aSRobin Jarry    Look into the provided binaries for DPDK_PLUGIN_PATH and scan the path
1690ce3cf4aSRobin Jarry    for files.
1700ce3cf4aSRobin Jarry    """
1710ce3cf4aSRobin Jarry    for b in binaries:
1720ce3cf4aSRobin Jarry        for p in get_elf_strings(b, ".rodata", "DPDK_PLUGIN_PATH="):
1730ce3cf4aSRobin Jarry            plugin_path = p.strip()
1740ce3cf4aSRobin Jarry            logging.debug("discovering plugins in %s", plugin_path)
1750ce3cf4aSRobin Jarry            for root, _, files in os.walk(plugin_path):
1760ce3cf4aSRobin Jarry                for f in files:
1770ce3cf4aSRobin Jarry                    yield Path(root) / f
1780ce3cf4aSRobin Jarry            # no need to search in other binaries.
1790ce3cf4aSRobin Jarry            return
1800ce3cf4aSRobin Jarry
1810ce3cf4aSRobin Jarry
1820ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
1830ce3cf4aSRobin Jarrydef existing_file(value: str) -> Path:
1840ce3cf4aSRobin Jarry    """
1850ce3cf4aSRobin Jarry    Argparse type= callback to ensure an argument points to a valid file path.
1860ce3cf4aSRobin Jarry    """
1870ce3cf4aSRobin Jarry    path = Path(value)
1880ce3cf4aSRobin Jarry    if not path.is_file():
1890ce3cf4aSRobin Jarry        raise argparse.ArgumentTypeError(f"{value}: No such file")
1900ce3cf4aSRobin Jarry    return path
1910ce3cf4aSRobin Jarry
1920ce3cf4aSRobin Jarry
1930ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
1940ce3cf4aSRobin JarryPRINTABLE_BYTES = frozenset(string.printable.encode("ascii"))
1950ce3cf4aSRobin Jarry
1960ce3cf4aSRobin Jarry
1970ce3cf4aSRobin Jarrydef find_strings(buf: bytes, prefix: str) -> Iterator[str]:
1980ce3cf4aSRobin Jarry    """
1990ce3cf4aSRobin Jarry    Extract strings of printable ASCII characters from a bytes buffer.
2000ce3cf4aSRobin Jarry    """
2010ce3cf4aSRobin Jarry    view = memoryview(buf)
2020ce3cf4aSRobin Jarry    start = None
2030ce3cf4aSRobin Jarry
2040ce3cf4aSRobin Jarry    for i, b in enumerate(view):
2050ce3cf4aSRobin Jarry        if start is None and b in PRINTABLE_BYTES:
2060ce3cf4aSRobin Jarry            # mark beginning of string
2070ce3cf4aSRobin Jarry            start = i
2080ce3cf4aSRobin Jarry            continue
2090ce3cf4aSRobin Jarry        if start is not None:
2100ce3cf4aSRobin Jarry            if b in PRINTABLE_BYTES:
2110ce3cf4aSRobin Jarry                # string not finished
2120ce3cf4aSRobin Jarry                continue
2130ce3cf4aSRobin Jarry            if b == 0:
2140ce3cf4aSRobin Jarry                # end of string
2150ce3cf4aSRobin Jarry                s = view[start:i].tobytes().decode("ascii")
2160ce3cf4aSRobin Jarry                if s.startswith(prefix):
2170ce3cf4aSRobin Jarry                    yield s[len(prefix):]
2180ce3cf4aSRobin Jarry            # There can be byte sequences where a non-printable byte
2190ce3cf4aSRobin Jarry            # follows a printable one. Ignore that.
2200ce3cf4aSRobin Jarry            start = None
2210ce3cf4aSRobin Jarry
2220ce3cf4aSRobin Jarry
2230ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
2240ce3cf4aSRobin Jarrydef elftools_version():
2250ce3cf4aSRobin Jarry    """
2260ce3cf4aSRobin Jarry    Extract pyelftools version as a tuple of integers for easy comparison.
2270ce3cf4aSRobin Jarry    """
2280ce3cf4aSRobin Jarry    version = getattr(elftools, "__version__", "")
2290ce3cf4aSRobin Jarry    match = re.match(r"^(\d+)\.(\d+).*$", str(version))
2300ce3cf4aSRobin Jarry    if not match:
2310ce3cf4aSRobin Jarry        # cannot determine version, hope for the best
2320ce3cf4aSRobin Jarry        return (0, 24)
2330ce3cf4aSRobin Jarry    return (int(match[1]), int(match[2]))
2340ce3cf4aSRobin Jarry
2350ce3cf4aSRobin Jarry
2360ce3cf4aSRobin JarryELFTOOLS_VERSION = elftools_version()
2370ce3cf4aSRobin Jarry
2380ce3cf4aSRobin Jarry
2390ce3cf4aSRobin Jarrydef from_elftools(s: Union[bytes, str]) -> str:
2400ce3cf4aSRobin Jarry    """
2410ce3cf4aSRobin Jarry    Earlier versions of pyelftools (< 0.24) return bytes encoded with "latin-1"
2420ce3cf4aSRobin Jarry    instead of python strings.
2430ce3cf4aSRobin Jarry    """
2440ce3cf4aSRobin Jarry    if isinstance(s, bytes):
2450ce3cf4aSRobin Jarry        return s.decode("latin-1")
2460ce3cf4aSRobin Jarry    return s
2470ce3cf4aSRobin Jarry
2480ce3cf4aSRobin Jarry
2490ce3cf4aSRobin Jarrydef to_elftools(s: str) -> Union[bytes, str]:
2500ce3cf4aSRobin Jarry    """
2510ce3cf4aSRobin Jarry    Earlier versions of pyelftools (< 0.24) assume that ELF section and tags
2520ce3cf4aSRobin Jarry    are bytes encoded with "latin-1" instead of python strings.
2530ce3cf4aSRobin Jarry    """
2540ce3cf4aSRobin Jarry    if ELFTOOLS_VERSION < (0, 24):
2550ce3cf4aSRobin Jarry        return s.encode("latin-1")
2560ce3cf4aSRobin Jarry    return s
2570ce3cf4aSRobin Jarry
2580ce3cf4aSRobin Jarry
2590ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
2600ce3cf4aSRobin Jarrydef get_elf_strings(path: Path, section: str, prefix: str) -> Iterator[str]:
2610ce3cf4aSRobin Jarry    """
2620ce3cf4aSRobin Jarry    Extract strings from a named ELF section in a file.
2630ce3cf4aSRobin Jarry    """
2640ce3cf4aSRobin Jarry    with path.open("rb") as f:
2650ce3cf4aSRobin Jarry        elf = ELFFile(f)
2660ce3cf4aSRobin Jarry        sec = elf.get_section_by_name(to_elftools(section))
2670ce3cf4aSRobin Jarry        if not sec:
2680ce3cf4aSRobin Jarry            return
2690ce3cf4aSRobin Jarry        yield from find_strings(sec.data(), prefix)
2700ce3cf4aSRobin Jarry
2710ce3cf4aSRobin Jarry
2720ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
2730ce3cf4aSRobin JarryLDD_LIB_RE = re.compile(
2740ce3cf4aSRobin Jarry    r"""
2750ce3cf4aSRobin Jarry    ^                  # beginning of line
2760ce3cf4aSRobin Jarry    \t                 # tab
2770ce3cf4aSRobin Jarry    (\S+)              # lib name
2780ce3cf4aSRobin Jarry    \s+=>\s+
2790ce3cf4aSRobin Jarry    (/\S+)             # lib path
2800ce3cf4aSRobin Jarry    \s+
2810ce3cf4aSRobin Jarry    \(0x[0-9A-Fa-f]+\) # address
2820ce3cf4aSRobin Jarry    \s*
2830ce3cf4aSRobin Jarry    $                  # end of line
2840ce3cf4aSRobin Jarry    """,
2850ce3cf4aSRobin Jarry    re.MULTILINE | re.VERBOSE,
2860ce3cf4aSRobin Jarry)
2870ce3cf4aSRobin Jarry
2880ce3cf4aSRobin Jarry
2890ce3cf4aSRobin Jarrydef get_needed_libs(path: Path) -> Iterator[Path]:
2900ce3cf4aSRobin Jarry    """
2910ce3cf4aSRobin Jarry    Extract the dynamic library dependencies from an ELF executable.
2920ce3cf4aSRobin Jarry    """
2930ce3cf4aSRobin Jarry    with subprocess.Popen(
2940ce3cf4aSRobin Jarry        ["ldd", str(path)], stdout=subprocess.PIPE, stderr=subprocess.PIPE
2950ce3cf4aSRobin Jarry    ) as proc:
2960ce3cf4aSRobin Jarry        out, err = proc.communicate()
2970ce3cf4aSRobin Jarry        if proc.returncode != 0:
2980ce3cf4aSRobin Jarry            err = err.decode("utf-8").splitlines()[-1].strip()
2990ce3cf4aSRobin Jarry            raise Exception(f"cannot read ELF file: {err}")
3000ce3cf4aSRobin Jarry        for match in LDD_LIB_RE.finditer(out.decode("utf-8")):
3010ce3cf4aSRobin Jarry            libname, libpath = match.groups()
3020ce3cf4aSRobin Jarry            if libname.startswith("librte_"):
3030ce3cf4aSRobin Jarry                libpath = Path(libpath)
3040ce3cf4aSRobin Jarry                if libpath.is_file():
3050ce3cf4aSRobin Jarry                    yield libpath.resolve()
3060ce3cf4aSRobin Jarry
3070ce3cf4aSRobin Jarry
3080ce3cf4aSRobin Jarry# ----------------------------------------------------------------------------
3090ce3cf4aSRobin Jarryif __name__ == "__main__":
3100ce3cf4aSRobin Jarry    sys.exit(main())
311