usertools: replace io.open
[dpdk.git] / usertools / dpdk-pmdinfo.py
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 # -------------------------------------------------------------------------
10 import json
11 import os
12 import platform
13 import sys
14 from elftools.common.exceptions import ELFError
15 from elftools.common.py3compat import byte2int
16 from elftools.elf.elffile import ELFFile
17 from optparse import OptionParser
18
19 # For running from development directory. It should take precedence over the
20 # installed pyelftools.
21 sys.path.insert(0, '.')
22
23 raw_output = False
24 pcidb = None
25
26 # ===========================================
27
28 class Vendor:
29     """
30     Class for vendors. This is the top level class
31     for the devices belong to a specific vendor.
32     self.devices is the device dictionary
33     subdevices are in each device.
34     """
35
36     def __init__(self, vendorStr):
37         """
38         Class initializes with the raw line from pci.ids
39         Parsing takes place inside __init__
40         """
41         self.ID = vendorStr.split()[0]
42         self.name = vendorStr.replace("%s " % self.ID, "").rstrip()
43         self.devices = {}
44
45     def addDevice(self, deviceStr):
46         """
47         Adds a device to self.devices
48         takes the raw line from pci.ids
49         """
50         s = deviceStr.strip()
51         devID = s.split()[0]
52         if devID in self.devices:
53             pass
54         else:
55             self.devices[devID] = Device(deviceStr)
56
57     def report(self):
58         print(self.ID, self.name)
59         for id, dev in self.devices.items():
60             dev.report()
61
62     def find_device(self, devid):
63         # convert to a hex string and remove 0x
64         devid = hex(devid)[2:]
65         try:
66             return self.devices[devid]
67         except:
68             return Device("%s  Unknown Device" % devid)
69
70
71 class Device:
72
73     def __init__(self, deviceStr):
74         """
75         Class for each device.
76         Each vendor has its own devices dictionary.
77         """
78         s = deviceStr.strip()
79         self.ID = s.split()[0]
80         self.name = s.replace("%s  " % self.ID, "")
81         self.subdevices = {}
82
83     def report(self):
84         print("\t%s\t%s" % (self.ID, self.name))
85         for subID, subdev in self.subdevices.items():
86             subdev.report()
87
88     def addSubDevice(self, subDeviceStr):
89         """
90         Adds a subvendor, subdevice to device.
91         Uses raw line from pci.ids
92         """
93         s = subDeviceStr.strip()
94         spl = s.split()
95         subVendorID = spl[0]
96         subDeviceID = spl[1]
97         subDeviceName = s.split("  ")[-1]
98         devID = "%s:%s" % (subVendorID, subDeviceID)
99         self.subdevices[devID] = SubDevice(
100             subVendorID, subDeviceID, subDeviceName)
101
102     def find_subid(self, subven, subdev):
103         subven = hex(subven)[2:]
104         subdev = hex(subdev)[2:]
105         devid = "%s:%s" % (subven, subdev)
106
107         try:
108             return self.subdevices[devid]
109         except:
110             if (subven == "ffff" and subdev == "ffff"):
111                 return SubDevice("ffff", "ffff", "(All Subdevices)")
112             else:
113                 return SubDevice(subven, subdev, "(Unknown Subdevice)")
114
115
116 class 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
133 class 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 len(self.contents) < 1:
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 len(l.strip()) == 0:
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
228 def 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
237 class 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             else:
265                 return None
266         except ValueError:
267             # Not a number. Must be a name then
268             section = self.elffile.get_section_by_name(force_unicode(spec))
269             if section is None:
270                 # No match with a unicode name.
271                 # Some versions of pyelftools (<= 0.23) store internal strings
272                 # as bytes. Try again with the name encoded as bytes.
273                 section = self.elffile.get_section_by_name(force_bytes(spec))
274             return section
275
276     def pretty_print_pmdinfo(self, pmdinfo):
277         global pcidb
278
279         for i in pmdinfo["pci_ids"]:
280             vendor = pcidb.find_vendor(i[0])
281             device = vendor.find_device(i[1])
282             subdev = device.find_subid(i[2], i[3])
283             print("%s (%s) : %s (%s) %s" %
284                   (vendor.name, vendor.ID, device.name,
285                    device.ID, subdev.name))
286
287     def parse_pmd_info_string(self, mystring):
288         global raw_output
289         global pcidb
290
291         optional_pmd_info = [
292             {'id': 'params', 'tag': 'PMD PARAMETERS'},
293             {'id': 'kmod', 'tag': 'PMD KMOD DEPENDENCIES'}
294         ]
295
296         i = mystring.index("=")
297         mystring = mystring[i + 2:]
298         pmdinfo = json.loads(mystring)
299
300         if raw_output:
301             print(json.dumps(pmdinfo))
302             return
303
304         print("PMD NAME: " + pmdinfo["name"])
305         for i in optional_pmd_info:
306             try:
307                 print("%s: %s" % (i['tag'], pmdinfo[i['id']]))
308             except KeyError:
309                 continue
310
311         if (len(pmdinfo["pci_ids"]) != 0):
312             print("PMD HW SUPPORT:")
313             if pcidb is not None:
314                 self.pretty_print_pmdinfo(pmdinfo)
315             else:
316                 print("VENDOR\t DEVICE\t SUBVENDOR\t SUBDEVICE")
317                 for i in pmdinfo["pci_ids"]:
318                     print("0x%04x\t 0x%04x\t 0x%04x\t\t 0x%04x" %
319                           (i[0], i[1], i[2], i[3]))
320
321         print("")
322
323     def display_pmd_info_strings(self, section_spec):
324         """ Display a strings dump of a section. section_spec is either a
325             section number or a name.
326         """
327         section = self._section_from_spec(section_spec)
328         if section is None:
329             return
330
331         data = section.data()
332         dataptr = 0
333
334         while dataptr < len(data):
335             while (dataptr < len(data) and
336                     not (32 <= byte2int(data[dataptr]) <= 127)):
337                 dataptr += 1
338
339             if dataptr >= len(data):
340                 break
341
342             endptr = dataptr
343             while endptr < len(data) and byte2int(data[endptr]) != 0:
344                 endptr += 1
345
346             # pyelftools may return byte-strings, force decode them
347             mystring = force_unicode(data[dataptr:endptr])
348             rc = mystring.find("PMD_INFO_STRING")
349             if (rc != -1):
350                 self.parse_pmd_info_string(mystring)
351
352             dataptr = endptr
353
354     def find_librte_eal(self, section):
355         for tag in section.iter_tags():
356             # pyelftools may return byte-strings, force decode them
357             if force_unicode(tag.entry.d_tag) == 'DT_NEEDED':
358                 if "librte_eal" in force_unicode(tag.needed):
359                     return force_unicode(tag.needed)
360         return None
361
362     def search_for_autoload_path(self):
363         scanelf = self
364         scanfile = None
365         library = None
366
367         section = self._section_from_spec(".dynamic")
368         try:
369             eallib = self.find_librte_eal(section)
370             if eallib is not None:
371                 ldlibpath = os.environ.get('LD_LIBRARY_PATH')
372                 if ldlibpath is None:
373                     ldlibpath = ""
374                 dtr = self.get_dt_runpath(section)
375                 library = search_file(eallib,
376                                       dtr + ":" + ldlibpath +
377                                       ":/usr/lib64:/lib64:/usr/lib:/lib")
378                 if library is None:
379                     return (None, None)
380                 if raw_output is False:
381                     print("Scanning for autoload path in %s" % library)
382                 scanfile = open(library, 'rb')
383                 scanelf = ReadElf(scanfile, sys.stdout)
384         except AttributeError:
385             # Not a dynamic binary
386             pass
387         except ELFError:
388             scanfile.close()
389             return (None, None)
390
391         section = scanelf._section_from_spec(".rodata")
392         if section is None:
393             if scanfile is not None:
394                 scanfile.close()
395             return (None, None)
396
397         data = section.data()
398         dataptr = 0
399
400         while dataptr < len(data):
401             while (dataptr < len(data) and
402                     not (32 <= byte2int(data[dataptr]) <= 127)):
403                 dataptr += 1
404
405             if dataptr >= len(data):
406                 break
407
408             endptr = dataptr
409             while endptr < len(data) and byte2int(data[endptr]) != 0:
410                 endptr += 1
411
412             # pyelftools may return byte-strings, force decode them
413             mystring = force_unicode(data[dataptr:endptr])
414             rc = mystring.find("DPDK_PLUGIN_PATH")
415             if (rc != -1):
416                 rc = mystring.find("=")
417                 return (mystring[rc + 1:], library)
418
419             dataptr = endptr
420         if scanfile is not None:
421             scanfile.close()
422         return (None, None)
423
424     def get_dt_runpath(self, dynsec):
425         for tag in dynsec.iter_tags():
426             # pyelftools may return byte-strings, force decode them
427             if force_unicode(tag.entry.d_tag) == 'DT_RUNPATH':
428                 return force_unicode(tag.runpath)
429         return ""
430
431     def process_dt_needed_entries(self):
432         """ Look to see if there are any DT_NEEDED entries in the binary
433             And process those if there are
434         """
435         runpath = ""
436         ldlibpath = os.environ.get('LD_LIBRARY_PATH')
437         if ldlibpath is None:
438             ldlibpath = ""
439
440         dynsec = self._section_from_spec(".dynamic")
441         try:
442             runpath = self.get_dt_runpath(dynsec)
443         except AttributeError:
444             # dynsec is None, just return
445             return
446
447         for tag in dynsec.iter_tags():
448             # pyelftools may return byte-strings, force decode them
449             if force_unicode(tag.entry.d_tag) == 'DT_NEEDED':
450                 if 'librte_' in force_unicode(tag.needed):
451                     library = search_file(force_unicode(tag.needed),
452                                           runpath + ":" + ldlibpath +
453                                           ":/usr/lib64:/lib64:/usr/lib:/lib")
454                     if library is not None:
455                         with open(library, 'rb') as file:
456                             try:
457                                 libelf = ReadElf(file, sys.stdout)
458                             except ELFError:
459                                 print("%s is no an ELF file" % library)
460                                 continue
461                             libelf.process_dt_needed_entries()
462                             libelf.display_pmd_info_strings(".rodata")
463                             file.close()
464
465
466 # compat: remove force_unicode & force_bytes when pyelftools<=0.23 support is
467 # dropped.
468 def force_unicode(s):
469     if hasattr(s, 'decode') and callable(s.decode):
470         s = s.decode('latin-1')  # same encoding used in pyelftools py3compat
471     return s
472
473
474 def force_bytes(s):
475     if hasattr(s, 'encode') and callable(s.encode):
476         s = s.encode('latin-1')  # same encoding used in pyelftools py3compat
477     return s
478
479
480 def scan_autoload_path(autoload_path):
481     global raw_output
482
483     if os.path.exists(autoload_path) is False:
484         return
485
486     try:
487         dirs = os.listdir(autoload_path)
488     except OSError:
489         # Couldn't read the directory, give up
490         return
491
492     for d in dirs:
493         dpath = os.path.join(autoload_path, d)
494         if os.path.isdir(dpath):
495             scan_autoload_path(dpath)
496         if os.path.isfile(dpath):
497             try:
498                 file = open(dpath, 'rb')
499                 readelf = ReadElf(file, sys.stdout)
500             except ELFError:
501                 # this is likely not an elf file, skip it
502                 continue
503             except IOError:
504                 # No permission to read the file, skip it
505                 continue
506
507             if raw_output is False:
508                 print("Hw Support for library %s" % d)
509             readelf.display_pmd_info_strings(".rodata")
510             file.close()
511
512
513 def scan_for_autoload_pmds(dpdk_path):
514     """
515     search the specified application or path for a pmd autoload path
516     then scan said path for pmds and report hw support
517     """
518     global raw_output
519
520     if (os.path.isfile(dpdk_path) is False):
521         if raw_output is False:
522             print("Must specify a file name")
523         return
524
525     file = open(dpdk_path, 'rb')
526     try:
527         readelf = ReadElf(file, sys.stdout)
528     except ElfError:
529         if raw_output is False:
530             print("Unable to parse %s" % file)
531         return
532
533     (autoload_path, scannedfile) = readelf.search_for_autoload_path()
534     if not autoload_path:
535         if (raw_output is False):
536             print("No autoload path configured in %s" % dpdk_path)
537         return
538     if (raw_output is False):
539         if (scannedfile is None):
540             scannedfile = dpdk_path
541         print("Found autoload path %s in %s" % (autoload_path, scannedfile))
542
543     file.close()
544     if (raw_output is False):
545         print("Discovered Autoload HW Support:")
546     scan_autoload_path(autoload_path)
547     return
548
549
550 def main(stream=None):
551     global raw_output
552     global pcidb
553
554     pcifile_default = "./pci.ids"  # For unknown OS's assume local file
555     if platform.system() == 'Linux':
556         # hwdata is the legacy location, misc is supported going forward
557         pcifile_default = "/usr/share/misc/pci.ids"
558         if not os.path.exists(pcifile_default):
559             pcifile_default = "/usr/share/hwdata/pci.ids"
560     elif platform.system() == 'FreeBSD':
561         pcifile_default = "/usr/local/share/pciids/pci.ids"
562         if not os.path.exists(pcifile_default):
563             pcifile_default = "/usr/share/misc/pci_vendors"
564
565     optparser = OptionParser(
566         usage='usage: %prog [-hrtp] [-d <pci id file] <elf-file>',
567         description="Dump pmd hardware support info",
568         add_help_option=True)
569     optparser.add_option('-r', '--raw',
570                          action='store_true', dest='raw_output',
571                          help='Dump raw json strings')
572     optparser.add_option("-d", "--pcidb", dest="pcifile",
573                          help="specify a pci database "
574                               "to get vendor names from",
575                          default=pcifile_default, metavar="FILE")
576     optparser.add_option("-t", "--table", dest="tblout",
577                          help="output information on hw support as a "
578                               "hex table",
579                          action='store_true')
580     optparser.add_option("-p", "--plugindir", dest="pdir",
581                          help="scan dpdk for autoload plugins",
582                          action='store_true')
583
584     options, args = optparser.parse_args()
585
586     if options.raw_output:
587         raw_output = True
588
589     if options.pcifile:
590         pcidb = PCIIds(options.pcifile)
591         if pcidb is None:
592             print("Pci DB file not found")
593             exit(1)
594
595     if options.tblout:
596         options.pcifile = None
597         pcidb = None
598
599     if (len(args) == 0):
600         optparser.print_usage()
601         exit(1)
602
603     if options.pdir is True:
604         exit(scan_for_autoload_pmds(args[0]))
605
606     ldlibpath = os.environ.get('LD_LIBRARY_PATH')
607     if (ldlibpath is None):
608         ldlibpath = ""
609
610     if (os.path.exists(args[0]) is True):
611         myelffile = args[0]
612     else:
613         myelffile = search_file(
614             args[0], ldlibpath + ":/usr/lib64:/lib64:/usr/lib:/lib")
615
616     if (myelffile is None):
617         print("File not found")
618         sys.exit(1)
619
620     with open(myelffile, 'rb') as file:
621         try:
622             readelf = ReadElf(file, sys.stdout)
623             readelf.process_dt_needed_entries()
624             readelf.display_pmd_info_strings(".rodata")
625             sys.exit(0)
626
627         except ELFError as ex:
628             sys.stderr.write('ELF error: %s\n' % ex)
629             sys.exit(1)
630
631
632 # -------------------------------------------------------------------------
633 if __name__ == '__main__':
634     main()