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