xref: /dpdk/usertools/dpdk-pmdinfo.py (revision bc8e32473cc3978d763a1387eaa8244bcf75e77d)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-3-Clause
3# Copyright(c) 2016  Neil Horman <nhorman@tuxdriver.com>
4
5# -------------------------------------------------------------------------
6#
7# Utility to dump PMD_INFO_STRING support from an object file
8#
9# -------------------------------------------------------------------------
10import json
11import io
12import os
13import platform
14import string
15import sys
16from elftools.common.exceptions import ELFError
17from elftools.common.py3compat import byte2int
18from elftools.elf.elffile import ELFFile
19from optparse import OptionParser
20
21# For running from development directory. It should take precedence over the
22# installed pyelftools.
23sys.path.insert(0, '.')
24
25raw_output = False
26pcidb = None
27
28# ===========================================
29
30class Vendor:
31    """
32    Class for vendors. This is the top level class
33    for the devices belong to a specific vendor.
34    self.devices is the device dictionary
35    subdevices are in each device.
36    """
37
38    def __init__(self, vendorStr):
39        """
40        Class initializes with the raw line from pci.ids
41        Parsing takes place inside __init__
42        """
43        self.ID = vendorStr.split()[0]
44        self.name = vendorStr.replace("%s " % self.ID, "").rstrip()
45        self.devices = {}
46
47    def addDevice(self, deviceStr):
48        """
49        Adds a device to self.devices
50        takes the raw line from pci.ids
51        """
52        s = deviceStr.strip()
53        devID = s.split()[0]
54        if devID in self.devices:
55            pass
56        else:
57            self.devices[devID] = Device(deviceStr)
58
59    def report(self):
60        print(self.ID, self.name)
61        for id, dev in self.devices.items():
62            dev.report()
63
64    def find_device(self, devid):
65        # convert to a hex string and remove 0x
66        devid = hex(devid)[2:]
67        try:
68            return self.devices[devid]
69        except:
70            return Device("%s  Unknown Device" % devid)
71
72
73class Device:
74
75    def __init__(self, deviceStr):
76        """
77        Class for each device.
78        Each vendor has its own devices dictionary.
79        """
80        s = deviceStr.strip()
81        self.ID = s.split()[0]
82        self.name = s.replace("%s  " % self.ID, "")
83        self.subdevices = {}
84
85    def report(self):
86        print("\t%s\t%s" % (self.ID, self.name))
87        for subID, subdev in self.subdevices.items():
88            subdev.report()
89
90    def addSubDevice(self, subDeviceStr):
91        """
92        Adds a subvendor, subdevice to device.
93        Uses raw line from pci.ids
94        """
95        s = subDeviceStr.strip()
96        spl = s.split()
97        subVendorID = spl[0]
98        subDeviceID = spl[1]
99        subDeviceName = s.split("  ")[-1]
100        devID = "%s:%s" % (subVendorID, subDeviceID)
101        self.subdevices[devID] = SubDevice(
102            subVendorID, subDeviceID, subDeviceName)
103
104    def find_subid(self, subven, subdev):
105        subven = hex(subven)[2:]
106        subdev = hex(subdev)[2:]
107        devid = "%s:%s" % (subven, subdev)
108
109        try:
110            return self.subdevices[devid]
111        except:
112            if (subven == "ffff" and subdev == "ffff"):
113                return SubDevice("ffff", "ffff", "(All Subdevices)")
114            else:
115                return SubDevice(subven, subdev, "(Unknown Subdevice)")
116
117
118class SubDevice:
119    """
120    Class for subdevices.
121    """
122
123    def __init__(self, vendor, device, name):
124        """
125        Class initializes with vendorid, deviceid and name
126        """
127        self.vendorID = vendor
128        self.deviceID = device
129        self.name = name
130
131    def report(self):
132        print("\t\t%s\t%s\t%s" % (self.vendorID, self.deviceID, self.name))
133
134
135class PCIIds:
136    """
137    Top class for all pci.ids entries.
138    All queries will be asked to this class.
139    PCIIds.vendors["0e11"].devices["0046"].\
140    subdevices["0e11:4091"].name  =  "Smart Array 6i"
141    """
142
143    def __init__(self, filename):
144        """
145        Prepares the directories.
146        Checks local data file.
147        Tries to load from local, if not found, downloads from web
148        """
149        self.version = ""
150        self.date = ""
151        self.vendors = {}
152        self.contents = None
153        self.readLocal(filename)
154        self.parse()
155
156    def reportVendors(self):
157        """Reports the vendors
158        """
159        for vid, v in self.vendors.items():
160            print(v.ID, v.name)
161
162    def report(self, vendor=None):
163        """
164        Reports everything for all vendors or a specific vendor
165        PCIIds.report()  reports everything
166        PCIIDs.report("0e11") reports only "Compaq Computer Corporation"
167        """
168        if vendor is not None:
169            self.vendors[vendor].report()
170        else:
171            for vID, v in self.vendors.items():
172                v.report()
173
174    def find_vendor(self, vid):
175        # convert vid to a hex string and remove the 0x
176        vid = hex(vid)[2:]
177
178        try:
179            return self.vendors[vid]
180        except:
181            return Vendor("%s Unknown Vendor" % (vid))
182
183    def findDate(self, content):
184        for l in content:
185            if l.find("Date:") > -1:
186                return l.split()[-2].replace("-", "")
187        return None
188
189    def parse(self):
190        if len(self.contents) < 1:
191            print("data/%s-pci.ids not found" % self.date)
192        else:
193            vendorID = ""
194            deviceID = ""
195            for l in self.contents:
196                if l[0] == "#":
197                    continue
198                elif len(l.strip()) == 0:
199                    continue
200                else:
201                    if l.find("\t\t") == 0:
202                        self.vendors[vendorID].devices[
203                            deviceID].addSubDevice(l)
204                    elif l.find("\t") == 0:
205                        deviceID = l.strip().split()[0]
206                        self.vendors[vendorID].addDevice(l)
207                    else:
208                        vendorID = l.split()[0]
209                        self.vendors[vendorID] = Vendor(l)
210
211    def readLocal(self, filename):
212        """
213        Reads the local file
214        """
215        with io.open(filename, 'r', encoding='utf-8') as f:
216            self.contents = f.readlines()
217        self.date = self.findDate(self.contents)
218
219    def loadLocal(self):
220        """
221        Loads database from local. If there is no file,
222        it creates a new one from web
223        """
224        self.date = idsfile[0].split("/")[1].split("-")[0]
225        self.readLocal()
226
227
228# =======================================
229
230def search_file(filename, search_path):
231    """ Given a search path, find file with requested name """
232    for path in string.split(search_path, ":"):
233        candidate = os.path.join(path, filename)
234        if os.path.exists(candidate):
235            return os.path.abspath(candidate)
236    return None
237
238
239class ReadElf(object):
240    """ display_* methods are used to emit output into the output stream
241    """
242
243    def __init__(self, file, output):
244        """ file:
245                stream object with the ELF file to read
246
247            output:
248                output stream to write to
249        """
250        self.elffile = ELFFile(file)
251        self.output = output
252
253        # Lazily initialized if a debug dump is requested
254        self._dwarfinfo = None
255
256        self._versioninfo = None
257
258    def _section_from_spec(self, spec):
259        """ Retrieve a section given a "spec" (either number or name).
260            Return None if no such section exists in the file.
261        """
262        try:
263            num = int(spec)
264            if num < self.elffile.num_sections():
265                return self.elffile.get_section(num)
266            else:
267                return None
268        except ValueError:
269            # Not a number. Must be a name then
270            section = self.elffile.get_section_by_name(force_unicode(spec))
271            if section is None:
272                # No match with a unicode name.
273                # Some versions of pyelftools (<= 0.23) store internal strings
274                # as bytes. Try again with the name encoded as bytes.
275                section = self.elffile.get_section_by_name(force_bytes(spec))
276            return section
277
278    def pretty_print_pmdinfo(self, pmdinfo):
279        global pcidb
280
281        for i in pmdinfo["pci_ids"]:
282            vendor = pcidb.find_vendor(i[0])
283            device = vendor.find_device(i[1])
284            subdev = device.find_subid(i[2], i[3])
285            print("%s (%s) : %s (%s) %s" %
286                  (vendor.name, vendor.ID, device.name,
287                   device.ID, subdev.name))
288
289    def parse_pmd_info_string(self, mystring):
290        global raw_output
291        global pcidb
292
293        optional_pmd_info = [
294            {'id': 'params', 'tag': 'PMD PARAMETERS'},
295            {'id': 'kmod', 'tag': 'PMD KMOD DEPENDENCIES'}
296        ]
297
298        i = mystring.index("=")
299        mystring = mystring[i + 2:]
300        pmdinfo = json.loads(mystring)
301
302        if raw_output:
303            print(json.dumps(pmdinfo))
304            return
305
306        print("PMD NAME: " + pmdinfo["name"])
307        for i in optional_pmd_info:
308            try:
309                print("%s: %s" % (i['tag'], pmdinfo[i['id']]))
310            except KeyError:
311                continue
312
313        if (len(pmdinfo["pci_ids"]) != 0):
314            print("PMD HW SUPPORT:")
315            if pcidb is not None:
316                self.pretty_print_pmdinfo(pmdinfo)
317            else:
318                print("VENDOR\t DEVICE\t SUBVENDOR\t SUBDEVICE")
319                for i in pmdinfo["pci_ids"]:
320                    print("0x%04x\t 0x%04x\t 0x%04x\t\t 0x%04x" %
321                          (i[0], i[1], i[2], i[3]))
322
323        print("")
324
325    def display_pmd_info_strings(self, section_spec):
326        """ Display a strings dump of a section. section_spec is either a
327            section number or a name.
328        """
329        section = self._section_from_spec(section_spec)
330        if section is None:
331            return
332
333        data = section.data()
334        dataptr = 0
335
336        while dataptr < len(data):
337            while (dataptr < len(data) and
338                    not (32 <= byte2int(data[dataptr]) <= 127)):
339                dataptr += 1
340
341            if dataptr >= len(data):
342                break
343
344            endptr = dataptr
345            while endptr < len(data) and byte2int(data[endptr]) != 0:
346                endptr += 1
347
348            # pyelftools may return byte-strings, force decode them
349            mystring = force_unicode(data[dataptr:endptr])
350            rc = mystring.find("PMD_INFO_STRING")
351            if (rc != -1):
352                self.parse_pmd_info_string(mystring)
353
354            dataptr = endptr
355
356    def find_librte_eal(self, section):
357        for tag in section.iter_tags():
358            # pyelftools may return byte-strings, force decode them
359            if force_unicode(tag.entry.d_tag) == 'DT_NEEDED':
360                if "librte_eal" in force_unicode(tag.needed):
361                    return force_unicode(tag.needed)
362        return None
363
364    def search_for_autoload_path(self):
365        scanelf = self
366        scanfile = None
367        library = None
368
369        section = self._section_from_spec(".dynamic")
370        try:
371            eallib = self.find_librte_eal(section)
372            if eallib is not None:
373                ldlibpath = os.environ.get('LD_LIBRARY_PATH')
374                if ldlibpath is None:
375                    ldlibpath = ""
376                dtr = self.get_dt_runpath(section)
377                library = search_file(eallib,
378                                      dtr + ":" + ldlibpath +
379                                      ":/usr/lib64:/lib64:/usr/lib:/lib")
380                if library is None:
381                    return (None, None)
382                if raw_output is False:
383                    print("Scanning for autoload path in %s" % library)
384                scanfile = io.open(library, 'rb')
385                scanelf = ReadElf(scanfile, sys.stdout)
386        except AttributeError:
387            # Not a dynamic binary
388            pass
389        except ELFError:
390            scanfile.close()
391            return (None, None)
392
393        section = scanelf._section_from_spec(".rodata")
394        if section is None:
395            if scanfile is not None:
396                scanfile.close()
397            return (None, None)
398
399        data = section.data()
400        dataptr = 0
401
402        while dataptr < len(data):
403            while (dataptr < len(data) and
404                    not (32 <= byte2int(data[dataptr]) <= 127)):
405                dataptr += 1
406
407            if dataptr >= len(data):
408                break
409
410            endptr = dataptr
411            while endptr < len(data) and byte2int(data[endptr]) != 0:
412                endptr += 1
413
414            # pyelftools may return byte-strings, force decode them
415            mystring = force_unicode(data[dataptr:endptr])
416            rc = mystring.find("DPDK_PLUGIN_PATH")
417            if (rc != -1):
418                rc = mystring.find("=")
419                return (mystring[rc + 1:], library)
420
421            dataptr = endptr
422        if scanfile is not None:
423            scanfile.close()
424        return (None, None)
425
426    def get_dt_runpath(self, dynsec):
427        for tag in dynsec.iter_tags():
428            # pyelftools may return byte-strings, force decode them
429            if force_unicode(tag.entry.d_tag) == 'DT_RUNPATH':
430                return force_unicode(tag.runpath)
431        return ""
432
433    def process_dt_needed_entries(self):
434        """ Look to see if there are any DT_NEEDED entries in the binary
435            And process those if there are
436        """
437        runpath = ""
438        ldlibpath = os.environ.get('LD_LIBRARY_PATH')
439        if ldlibpath is None:
440            ldlibpath = ""
441
442        dynsec = self._section_from_spec(".dynamic")
443        try:
444            runpath = self.get_dt_runpath(dynsec)
445        except AttributeError:
446            # dynsec is None, just return
447            return
448
449        for tag in dynsec.iter_tags():
450            # pyelftools may return byte-strings, force decode them
451            if force_unicode(tag.entry.d_tag) == 'DT_NEEDED':
452                if 'librte_' in force_unicode(tag.needed):
453                    library = search_file(force_unicode(tag.needed),
454                                          runpath + ":" + ldlibpath +
455                                          ":/usr/lib64:/lib64:/usr/lib:/lib")
456                    if library is not None:
457                        with io.open(library, 'rb') as file:
458                            try:
459                                libelf = ReadElf(file, sys.stdout)
460                            except ELFError:
461                                print("%s is no an ELF file" % library)
462                                continue
463                            libelf.process_dt_needed_entries()
464                            libelf.display_pmd_info_strings(".rodata")
465                            file.close()
466
467
468# compat: remove force_unicode & force_bytes when pyelftools<=0.23 support is
469# dropped.
470def force_unicode(s):
471    if hasattr(s, 'decode') and callable(s.decode):
472        s = s.decode('latin-1')  # same encoding used in pyelftools py3compat
473    return s
474
475
476def force_bytes(s):
477    if hasattr(s, 'encode') and callable(s.encode):
478        s = s.encode('latin-1')  # same encoding used in pyelftools py3compat
479    return s
480
481
482def scan_autoload_path(autoload_path):
483    global raw_output
484
485    if os.path.exists(autoload_path) is False:
486        return
487
488    try:
489        dirs = os.listdir(autoload_path)
490    except OSError:
491        # Couldn't read the directory, give up
492        return
493
494    for d in dirs:
495        dpath = os.path.join(autoload_path, d)
496        if os.path.isdir(dpath):
497            scan_autoload_path(dpath)
498        if os.path.isfile(dpath):
499            try:
500                file = io.open(dpath, 'rb')
501                readelf = ReadElf(file, sys.stdout)
502            except ELFError:
503                # this is likely not an elf file, skip it
504                continue
505            except IOError:
506                # No permission to read the file, skip it
507                continue
508
509            if raw_output is False:
510                print("Hw Support for library %s" % d)
511            readelf.display_pmd_info_strings(".rodata")
512            file.close()
513
514
515def scan_for_autoload_pmds(dpdk_path):
516    """
517    search the specified application or path for a pmd autoload path
518    then scan said path for pmds and report hw support
519    """
520    global raw_output
521
522    if (os.path.isfile(dpdk_path) is False):
523        if raw_output is False:
524            print("Must specify a file name")
525        return
526
527    file = io.open(dpdk_path, 'rb')
528    try:
529        readelf = ReadElf(file, sys.stdout)
530    except ElfError:
531        if raw_output is False:
532            print("Unable to parse %s" % file)
533        return
534
535    (autoload_path, scannedfile) = readelf.search_for_autoload_path()
536    if not autoload_path:
537        if (raw_output is False):
538            print("No autoload path configured in %s" % dpdk_path)
539        return
540    if (raw_output is False):
541        if (scannedfile is None):
542            scannedfile = dpdk_path
543        print("Found autoload path %s in %s" % (autoload_path, scannedfile))
544
545    file.close()
546    if (raw_output is False):
547        print("Discovered Autoload HW Support:")
548    scan_autoload_path(autoload_path)
549    return
550
551
552def main(stream=None):
553    global raw_output
554    global pcidb
555
556    pcifile_default = "./pci.ids"  # For unknown OS's assume local file
557    if platform.system() == 'Linux':
558        # hwdata is the legacy location, misc is supported going forward
559        pcifile_default = "/usr/share/misc/pci.ids"
560        if not os.path.exists(pcifile_default):
561            pcifile_default = "/usr/share/hwdata/pci.ids"
562    elif platform.system() == 'FreeBSD':
563        pcifile_default = "/usr/local/share/pciids/pci.ids"
564        if not os.path.exists(pcifile_default):
565            pcifile_default = "/usr/share/misc/pci_vendors"
566
567    optparser = OptionParser(
568        usage='usage: %prog [-hrtp] [-d <pci id file] <elf-file>',
569        description="Dump pmd hardware support info",
570        add_help_option=True)
571    optparser.add_option('-r', '--raw',
572                         action='store_true', dest='raw_output',
573                         help='Dump raw json strings')
574    optparser.add_option("-d", "--pcidb", dest="pcifile",
575                         help="specify a pci database "
576                              "to get vendor names from",
577                         default=pcifile_default, metavar="FILE")
578    optparser.add_option("-t", "--table", dest="tblout",
579                         help="output information on hw support as a "
580                              "hex table",
581                         action='store_true')
582    optparser.add_option("-p", "--plugindir", dest="pdir",
583                         help="scan dpdk for autoload plugins",
584                         action='store_true')
585
586    options, args = optparser.parse_args()
587
588    if options.raw_output:
589        raw_output = True
590
591    if options.pcifile:
592        pcidb = PCIIds(options.pcifile)
593        if pcidb is None:
594            print("Pci DB file not found")
595            exit(1)
596
597    if options.tblout:
598        options.pcifile = None
599        pcidb = None
600
601    if (len(args) == 0):
602        optparser.print_usage()
603        exit(1)
604
605    if options.pdir is True:
606        exit(scan_for_autoload_pmds(args[0]))
607
608    ldlibpath = os.environ.get('LD_LIBRARY_PATH')
609    if (ldlibpath is None):
610        ldlibpath = ""
611
612    if (os.path.exists(args[0]) is True):
613        myelffile = args[0]
614    else:
615        myelffile = search_file(
616            args[0], ldlibpath + ":/usr/lib64:/lib64:/usr/lib:/lib")
617
618    if (myelffile is None):
619        print("File not found")
620        sys.exit(1)
621
622    with io.open(myelffile, 'rb') as file:
623        try:
624            readelf = ReadElf(file, sys.stdout)
625            readelf.process_dt_needed_entries()
626            readelf.display_pmd_info_strings(".rodata")
627            sys.exit(0)
628
629        except ELFError as ex:
630            sys.stderr.write('ELF error: %s\n' % ex)
631            sys.exit(1)
632
633
634# -------------------------------------------------------------------------
635if __name__ == '__main__':
636    main()
637