Merge tag 'trace-tools-v5.19' of git://git.kernel.org/pub/scm/linux/kernel/git/rosted...
[linux-2.6-microblaze.git] / tools / testing / selftests / bpf / test_bpftool_synctypes.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
3 #
4 # Copyright (C) 2021 Isovalent, Inc.
5
6 import argparse
7 import re
8 import os, sys
9
10 LINUX_ROOT = os.path.abspath(os.path.join(__file__,
11     os.pardir, os.pardir, os.pardir, os.pardir, os.pardir))
12 BPFTOOL_DIR = os.getenv('BPFTOOL_DIR',
13     os.path.join(LINUX_ROOT, 'tools/bpf/bpftool'))
14 BPFTOOL_BASHCOMP_DIR = os.getenv('BPFTOOL_BASHCOMP_DIR',
15     os.path.join(BPFTOOL_DIR, 'bash-completion'))
16 BPFTOOL_DOC_DIR = os.getenv('BPFTOOL_DOC_DIR',
17     os.path.join(BPFTOOL_DIR, 'Documentation'))
18 INCLUDE_DIR = os.getenv('INCLUDE_DIR',
19     os.path.join(LINUX_ROOT, 'tools/include'))
20
21 retval = 0
22
23 class BlockParser(object):
24     """
25     A parser for extracting set of values from blocks such as enums.
26     @reader: a pointer to the open file to parse
27     """
28     def __init__(self, reader):
29         self.reader = reader
30
31     def search_block(self, start_marker):
32         """
33         Search for a given structure in a file.
34         @start_marker: regex marking the beginning of a structure to parse
35         """
36         offset = self.reader.tell()
37         array_start = re.search(start_marker, self.reader.read())
38         if array_start is None:
39             raise Exception('Failed to find start of block')
40         self.reader.seek(offset + array_start.start())
41
42     def parse(self, pattern, end_marker):
43         """
44         Parse a block and return a set of values. Values to extract must be
45         on separate lines in the file.
46         @pattern: pattern used to identify the values to extract
47         @end_marker: regex marking the end of the block to parse
48         """
49         entries = set()
50         while True:
51             line = self.reader.readline()
52             if not line or re.match(end_marker, line):
53                 break
54             capture = pattern.search(line)
55             if capture and pattern.groups >= 1:
56                 entries.add(capture.group(1))
57         return entries
58
59 class ArrayParser(BlockParser):
60     """
61     A parser for extracting dicionaries of values from some BPF-related arrays.
62     @reader: a pointer to the open file to parse
63     @array_name: name of the array to parse
64     """
65     end_marker = re.compile('^};')
66
67     def __init__(self, reader, array_name):
68         self.array_name = array_name
69         self.start_marker = re.compile(f'(static )?const char \* const {self.array_name}\[.*\] = {{\n')
70         super().__init__(reader)
71
72     def search_block(self):
73         """
74         Search for the given array in a file.
75         """
76         super().search_block(self.start_marker);
77
78     def parse(self):
79         """
80         Parse a block and return data as a dictionary. Items to extract must be
81         on separate lines in the file.
82         """
83         pattern = re.compile('\[(BPF_\w*)\]\s*= "(.*)",?$')
84         entries = {}
85         while True:
86             line = self.reader.readline()
87             if line == '' or re.match(self.end_marker, line):
88                 break
89             capture = pattern.search(line)
90             if capture:
91                 entries[capture.group(1)] = capture.group(2)
92         return entries
93
94 class InlineListParser(BlockParser):
95     """
96     A parser for extracting set of values from inline lists.
97     """
98     def parse(self, pattern, end_marker):
99         """
100         Parse a block and return a set of values. Multiple values to extract
101         can be on a same line in the file.
102         @pattern: pattern used to identify the values to extract
103         @end_marker: regex marking the end of the block to parse
104         """
105         entries = set()
106         while True:
107             line = self.reader.readline()
108             if not line:
109                 break
110             entries.update(pattern.findall(line))
111             if re.search(end_marker, line):
112                 break
113         return entries
114
115 class FileExtractor(object):
116     """
117     A generic reader for extracting data from a given file. This class contains
118     several helper methods that wrap arround parser objects to extract values
119     from different structures.
120     This class does not offer a way to set a filename, which is expected to be
121     defined in children classes.
122     """
123     def __init__(self):
124         self.reader = open(self.filename, 'r')
125
126     def close(self):
127         """
128         Close the file used by the parser.
129         """
130         self.reader.close()
131
132     def reset_read(self):
133         """
134         Reset the file position indicator for this parser. This is useful when
135         parsing several structures in the file without respecting the order in
136         which those structures appear in the file.
137         """
138         self.reader.seek(0)
139
140     def get_types_from_array(self, array_name):
141         """
142         Search for and parse an array associating names to BPF_* enum members,
143         for example:
144
145             const char * const prog_type_name[] = {
146                     [BPF_PROG_TYPE_UNSPEC]                  = "unspec",
147                     [BPF_PROG_TYPE_SOCKET_FILTER]           = "socket_filter",
148                     [BPF_PROG_TYPE_KPROBE]                  = "kprobe",
149             };
150
151         Return a dictionary with the enum member names as keys and the
152         associated names as values, for example:
153
154             {'BPF_PROG_TYPE_UNSPEC': 'unspec',
155              'BPF_PROG_TYPE_SOCKET_FILTER': 'socket_filter',
156              'BPF_PROG_TYPE_KPROBE': 'kprobe'}
157
158         @array_name: name of the array to parse
159         """
160         array_parser = ArrayParser(self.reader, array_name)
161         array_parser.search_block()
162         return array_parser.parse()
163
164     def get_enum(self, enum_name):
165         """
166         Search for and parse an enum containing BPF_* members, for example:
167
168             enum bpf_prog_type {
169                     BPF_PROG_TYPE_UNSPEC,
170                     BPF_PROG_TYPE_SOCKET_FILTER,
171                     BPF_PROG_TYPE_KPROBE,
172             };
173
174         Return a set containing all member names, for example:
175
176             {'BPF_PROG_TYPE_UNSPEC',
177              'BPF_PROG_TYPE_SOCKET_FILTER',
178              'BPF_PROG_TYPE_KPROBE'}
179
180         @enum_name: name of the enum to parse
181         """
182         start_marker = re.compile(f'enum {enum_name} {{\n')
183         pattern = re.compile('^\s*(BPF_\w+),?(\s+/\*.*\*/)?$')
184         end_marker = re.compile('^};')
185         parser = BlockParser(self.reader)
186         parser.search_block(start_marker)
187         return parser.parse(pattern, end_marker)
188
189     def __get_description_list(self, start_marker, pattern, end_marker):
190         parser = InlineListParser(self.reader)
191         parser.search_block(start_marker)
192         return parser.parse(pattern, end_marker)
193
194     def get_rst_list(self, block_name):
195         """
196         Search for and parse a list of type names from RST documentation, for
197         example:
198
199              |       *TYPE* := {
200              |               **socket** | **kprobe** |
201              |               **kretprobe**
202              |       }
203
204         Return a set containing all type names, for example:
205
206             {'socket', 'kprobe', 'kretprobe'}
207
208         @block_name: name of the blog to parse, 'TYPE' in the example
209         """
210         start_marker = re.compile(f'\*{block_name}\* := {{')
211         pattern = re.compile('\*\*([\w/-]+)\*\*')
212         end_marker = re.compile('}\n')
213         return self.__get_description_list(start_marker, pattern, end_marker)
214
215     def get_help_list(self, block_name):
216         """
217         Search for and parse a list of type names from a help message in
218         bpftool, for example:
219
220             "       TYPE := { socket | kprobe |\\n"
221             "               kretprobe }\\n"
222
223         Return a set containing all type names, for example:
224
225             {'socket', 'kprobe', 'kretprobe'}
226
227         @block_name: name of the blog to parse, 'TYPE' in the example
228         """
229         start_marker = re.compile(f'"\s*{block_name} := {{')
230         pattern = re.compile('([\w/]+) [|}]')
231         end_marker = re.compile('}')
232         return self.__get_description_list(start_marker, pattern, end_marker)
233
234     def get_help_list_macro(self, macro):
235         """
236         Search for and parse a list of values from a help message starting with
237         a macro in bpftool, for example:
238
239             "       " HELP_SPEC_OPTIONS " |\\n"
240             "                    {-f|--bpffs} | {-m|--mapcompat} | {-n|--nomount} }\\n"
241
242         Return a set containing all item names, for example:
243
244             {'-f', '--bpffs', '-m', '--mapcompat', '-n', '--nomount'}
245
246         @macro: macro starting the block, 'HELP_SPEC_OPTIONS' in the example
247         """
248         start_marker = re.compile(f'"\s*{macro}\s*" [|}}]')
249         pattern = re.compile('([\w-]+) ?(?:\||}[ }\]])')
250         end_marker = re.compile('}\\\\n')
251         return self.__get_description_list(start_marker, pattern, end_marker)
252
253     def get_bashcomp_list(self, block_name):
254         """
255         Search for and parse a list of type names from a variable in bash
256         completion file, for example:
257
258             local BPFTOOL_PROG_LOAD_TYPES='socket kprobe \\
259                 kretprobe'
260
261         Return a set containing all type names, for example:
262
263             {'socket', 'kprobe', 'kretprobe'}
264
265         @block_name: name of the blog to parse, 'TYPE' in the example
266         """
267         start_marker = re.compile(f'local {block_name}=\'')
268         pattern = re.compile('(?:.*=\')?([\w/]+)')
269         end_marker = re.compile('\'$')
270         return self.__get_description_list(start_marker, pattern, end_marker)
271
272 class SourceFileExtractor(FileExtractor):
273     """
274     An abstract extractor for a source file with usage message.
275     This class does not offer a way to set a filename, which is expected to be
276     defined in children classes.
277     """
278     def get_options(self):
279         return self.get_help_list_macro('HELP_SPEC_OPTIONS')
280
281 class MainHeaderFileExtractor(SourceFileExtractor):
282     """
283     An extractor for bpftool's main.h
284     """
285     filename = os.path.join(BPFTOOL_DIR, 'main.h')
286
287     def get_common_options(self):
288         """
289         Parse the list of common options in main.h (options that apply to all
290         commands), which looks to the lists of options in other source files
291         but has different start and end markers:
292
293             "OPTIONS := { {-j|--json} [{-p|--pretty}] | {-d|--debug} | {-l|--legacy}"
294
295         Return a set containing all options, such as:
296
297             {'-p', '-d', '--legacy', '--pretty', '--debug', '--json', '-l', '-j'}
298         """
299         start_marker = re.compile(f'"OPTIONS :=')
300         pattern = re.compile('([\w-]+) ?(?:\||}[ }\]"])')
301         end_marker = re.compile('#define')
302
303         parser = InlineListParser(self.reader)
304         parser.search_block(start_marker)
305         return parser.parse(pattern, end_marker)
306
307 class ManSubstitutionsExtractor(SourceFileExtractor):
308     """
309     An extractor for substitutions.rst
310     """
311     filename = os.path.join(BPFTOOL_DOC_DIR, 'substitutions.rst')
312
313     def get_common_options(self):
314         """
315         Parse the list of common options in substitutions.rst (options that
316         apply to all commands).
317
318         Return a set containing all options, such as:
319
320             {'-p', '-d', '--legacy', '--pretty', '--debug', '--json', '-l', '-j'}
321         """
322         start_marker = re.compile('\|COMMON_OPTIONS\| replace:: {')
323         pattern = re.compile('\*\*([\w/-]+)\*\*')
324         end_marker = re.compile('}$')
325
326         parser = InlineListParser(self.reader)
327         parser.search_block(start_marker)
328         return parser.parse(pattern, end_marker)
329
330 class ProgFileExtractor(SourceFileExtractor):
331     """
332     An extractor for bpftool's prog.c.
333     """
334     filename = os.path.join(BPFTOOL_DIR, 'prog.c')
335
336     def get_prog_types(self):
337         return self.get_types_from_array('prog_type_name')
338
339     def get_attach_types(self):
340         return self.get_types_from_array('attach_type_strings')
341
342     def get_prog_attach_help(self):
343         return self.get_help_list('ATTACH_TYPE')
344
345 class MapFileExtractor(SourceFileExtractor):
346     """
347     An extractor for bpftool's map.c.
348     """
349     filename = os.path.join(BPFTOOL_DIR, 'map.c')
350
351     def get_map_types(self):
352         return self.get_types_from_array('map_type_name')
353
354     def get_map_help(self):
355         return self.get_help_list('TYPE')
356
357 class CgroupFileExtractor(SourceFileExtractor):
358     """
359     An extractor for bpftool's cgroup.c.
360     """
361     filename = os.path.join(BPFTOOL_DIR, 'cgroup.c')
362
363     def get_prog_attach_help(self):
364         return self.get_help_list('ATTACH_TYPE')
365
366 class CommonFileExtractor(SourceFileExtractor):
367     """
368     An extractor for bpftool's common.c.
369     """
370     filename = os.path.join(BPFTOOL_DIR, 'common.c')
371
372     def __init__(self):
373         super().__init__()
374         self.attach_types = {}
375
376     def get_attach_types(self):
377         if not self.attach_types:
378             self.attach_types = self.get_types_from_array('attach_type_name')
379         return self.attach_types
380
381     def get_cgroup_attach_types(self):
382         if not self.attach_types:
383             self.get_attach_types()
384         cgroup_types = {}
385         for (key, value) in self.attach_types.items():
386             if key.find('BPF_CGROUP') != -1:
387                 cgroup_types[key] = value
388         return cgroup_types
389
390 class GenericSourceExtractor(SourceFileExtractor):
391     """
392     An extractor for generic source code files.
393     """
394     filename = ""
395
396     def __init__(self, filename):
397         self.filename = os.path.join(BPFTOOL_DIR, filename)
398         super().__init__()
399
400 class BpfHeaderExtractor(FileExtractor):
401     """
402     An extractor for the UAPI BPF header.
403     """
404     filename = os.path.join(INCLUDE_DIR, 'uapi/linux/bpf.h')
405
406     def get_prog_types(self):
407         return self.get_enum('bpf_prog_type')
408
409     def get_map_types(self):
410         return self.get_enum('bpf_map_type')
411
412     def get_attach_types(self):
413         return self.get_enum('bpf_attach_type')
414
415 class ManPageExtractor(FileExtractor):
416     """
417     An abstract extractor for an RST documentation page.
418     This class does not offer a way to set a filename, which is expected to be
419     defined in children classes.
420     """
421     def get_options(self):
422         return self.get_rst_list('OPTIONS')
423
424 class ManProgExtractor(ManPageExtractor):
425     """
426     An extractor for bpftool-prog.rst.
427     """
428     filename = os.path.join(BPFTOOL_DOC_DIR, 'bpftool-prog.rst')
429
430     def get_attach_types(self):
431         return self.get_rst_list('ATTACH_TYPE')
432
433 class ManMapExtractor(ManPageExtractor):
434     """
435     An extractor for bpftool-map.rst.
436     """
437     filename = os.path.join(BPFTOOL_DOC_DIR, 'bpftool-map.rst')
438
439     def get_map_types(self):
440         return self.get_rst_list('TYPE')
441
442 class ManCgroupExtractor(ManPageExtractor):
443     """
444     An extractor for bpftool-cgroup.rst.
445     """
446     filename = os.path.join(BPFTOOL_DOC_DIR, 'bpftool-cgroup.rst')
447
448     def get_attach_types(self):
449         return self.get_rst_list('ATTACH_TYPE')
450
451 class ManGenericExtractor(ManPageExtractor):
452     """
453     An extractor for generic RST documentation pages.
454     """
455     filename = ""
456
457     def __init__(self, filename):
458         self.filename = os.path.join(BPFTOOL_DIR, filename)
459         super().__init__()
460
461 class BashcompExtractor(FileExtractor):
462     """
463     An extractor for bpftool's bash completion file.
464     """
465     filename = os.path.join(BPFTOOL_BASHCOMP_DIR, 'bpftool')
466
467     def get_prog_attach_types(self):
468         return self.get_bashcomp_list('BPFTOOL_PROG_ATTACH_TYPES')
469
470     def get_map_types(self):
471         return self.get_bashcomp_list('BPFTOOL_MAP_CREATE_TYPES')
472
473     def get_cgroup_attach_types(self):
474         return self.get_bashcomp_list('BPFTOOL_CGROUP_ATTACH_TYPES')
475
476 def verify(first_set, second_set, message):
477     """
478     Print all values that differ between two sets.
479     @first_set: one set to compare
480     @second_set: another set to compare
481     @message: message to print for values belonging to only one of the sets
482     """
483     global retval
484     diff = first_set.symmetric_difference(second_set)
485     if diff:
486         print(message, diff)
487         retval = 1
488
489 def main():
490     # No arguments supported at this time, but print usage for -h|--help
491     argParser = argparse.ArgumentParser(description="""
492     Verify that bpftool's code, help messages, documentation and bash
493     completion are all in sync on program types, map types, attach types, and
494     options. Also check that bpftool is in sync with the UAPI BPF header.
495     """)
496     args = argParser.parse_args()
497
498     # Map types (enum)
499
500     bpf_info = BpfHeaderExtractor()
501     ref = bpf_info.get_map_types()
502
503     map_info = MapFileExtractor()
504     source_map_items = map_info.get_map_types()
505     map_types_enum = set(source_map_items.keys())
506
507     verify(ref, map_types_enum,
508             f'Comparing BPF header (enum bpf_map_type) and {MapFileExtractor.filename} (map_type_name):')
509
510     # Map types (names)
511
512     source_map_types = set(source_map_items.values())
513     source_map_types.discard('unspec')
514
515     help_map_types = map_info.get_map_help()
516     help_map_options = map_info.get_options()
517     map_info.close()
518
519     man_map_info = ManMapExtractor()
520     man_map_options = man_map_info.get_options()
521     man_map_types = man_map_info.get_map_types()
522     man_map_info.close()
523
524     bashcomp_info = BashcompExtractor()
525     bashcomp_map_types = bashcomp_info.get_map_types()
526
527     verify(source_map_types, help_map_types,
528             f'Comparing {MapFileExtractor.filename} (map_type_name) and {MapFileExtractor.filename} (do_help() TYPE):')
529     verify(source_map_types, man_map_types,
530             f'Comparing {MapFileExtractor.filename} (map_type_name) and {ManMapExtractor.filename} (TYPE):')
531     verify(help_map_options, man_map_options,
532             f'Comparing {MapFileExtractor.filename} (do_help() OPTIONS) and {ManMapExtractor.filename} (OPTIONS):')
533     verify(source_map_types, bashcomp_map_types,
534             f'Comparing {MapFileExtractor.filename} (map_type_name) and {BashcompExtractor.filename} (BPFTOOL_MAP_CREATE_TYPES):')
535
536     # Program types (enum)
537
538     ref = bpf_info.get_prog_types()
539
540     prog_info = ProgFileExtractor()
541     prog_types = set(prog_info.get_prog_types().keys())
542
543     verify(ref, prog_types,
544             f'Comparing BPF header (enum bpf_prog_type) and {ProgFileExtractor.filename} (prog_type_name):')
545
546     # Attach types (enum)
547
548     ref = bpf_info.get_attach_types()
549     bpf_info.close()
550
551     common_info = CommonFileExtractor()
552     attach_types = common_info.get_attach_types()
553
554     verify(ref, attach_types,
555             f'Comparing BPF header (enum bpf_attach_type) and {CommonFileExtractor.filename} (attach_type_name):')
556
557     # Attach types (names)
558
559     source_prog_attach_types = set(prog_info.get_attach_types().values())
560
561     help_prog_attach_types = prog_info.get_prog_attach_help()
562     help_prog_options = prog_info.get_options()
563     prog_info.close()
564
565     man_prog_info = ManProgExtractor()
566     man_prog_options = man_prog_info.get_options()
567     man_prog_attach_types = man_prog_info.get_attach_types()
568     man_prog_info.close()
569
570     bashcomp_info.reset_read() # We stopped at map types, rewind
571     bashcomp_prog_attach_types = bashcomp_info.get_prog_attach_types()
572
573     verify(source_prog_attach_types, help_prog_attach_types,
574             f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {ProgFileExtractor.filename} (do_help() ATTACH_TYPE):')
575     verify(source_prog_attach_types, man_prog_attach_types,
576             f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {ManProgExtractor.filename} (ATTACH_TYPE):')
577     verify(help_prog_options, man_prog_options,
578             f'Comparing {ProgFileExtractor.filename} (do_help() OPTIONS) and {ManProgExtractor.filename} (OPTIONS):')
579     verify(source_prog_attach_types, bashcomp_prog_attach_types,
580             f'Comparing {ProgFileExtractor.filename} (attach_type_strings) and {BashcompExtractor.filename} (BPFTOOL_PROG_ATTACH_TYPES):')
581
582     # Cgroup attach types
583
584     source_cgroup_attach_types = set(common_info.get_cgroup_attach_types().values())
585     common_info.close()
586
587     cgroup_info = CgroupFileExtractor()
588     help_cgroup_attach_types = cgroup_info.get_prog_attach_help()
589     help_cgroup_options = cgroup_info.get_options()
590     cgroup_info.close()
591
592     man_cgroup_info = ManCgroupExtractor()
593     man_cgroup_options = man_cgroup_info.get_options()
594     man_cgroup_attach_types = man_cgroup_info.get_attach_types()
595     man_cgroup_info.close()
596
597     bashcomp_cgroup_attach_types = bashcomp_info.get_cgroup_attach_types()
598     bashcomp_info.close()
599
600     verify(source_cgroup_attach_types, help_cgroup_attach_types,
601             f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {CgroupFileExtractor.filename} (do_help() ATTACH_TYPE):')
602     verify(source_cgroup_attach_types, man_cgroup_attach_types,
603             f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {ManCgroupExtractor.filename} (ATTACH_TYPE):')
604     verify(help_cgroup_options, man_cgroup_options,
605             f'Comparing {CgroupFileExtractor.filename} (do_help() OPTIONS) and {ManCgroupExtractor.filename} (OPTIONS):')
606     verify(source_cgroup_attach_types, bashcomp_cgroup_attach_types,
607             f'Comparing {CommonFileExtractor.filename} (attach_type_strings) and {BashcompExtractor.filename} (BPFTOOL_CGROUP_ATTACH_TYPES):')
608
609     # Options for remaining commands
610
611     for cmd in [ 'btf', 'feature', 'gen', 'iter', 'link', 'net', 'perf', 'struct_ops', ]:
612         source_info = GenericSourceExtractor(cmd + '.c')
613         help_cmd_options = source_info.get_options()
614         source_info.close()
615
616         man_cmd_info = ManGenericExtractor(os.path.join(BPFTOOL_DOC_DIR, 'bpftool-' + cmd + '.rst'))
617         man_cmd_options = man_cmd_info.get_options()
618         man_cmd_info.close()
619
620         verify(help_cmd_options, man_cmd_options,
621                 f'Comparing {source_info.filename} (do_help() OPTIONS) and {man_cmd_info.filename} (OPTIONS):')
622
623     source_main_info = GenericSourceExtractor('main.c')
624     help_main_options = source_main_info.get_options()
625     source_main_info.close()
626
627     man_main_info = ManGenericExtractor(os.path.join(BPFTOOL_DOC_DIR, 'bpftool.rst'))
628     man_main_options = man_main_info.get_options()
629     man_main_info.close()
630
631     verify(help_main_options, man_main_options,
632             f'Comparing {source_main_info.filename} (do_help() OPTIONS) and {man_main_info.filename} (OPTIONS):')
633
634     # Compare common options (options that apply to all commands)
635
636     main_hdr_info = MainHeaderFileExtractor()
637     source_common_options = main_hdr_info.get_common_options()
638     main_hdr_info.close()
639
640     man_substitutions = ManSubstitutionsExtractor()
641     man_common_options = man_substitutions.get_common_options()
642     man_substitutions.close()
643
644     verify(source_common_options, man_common_options,
645             f'Comparing common options from {main_hdr_info.filename} (HELP_SPEC_OPTIONS) and {man_substitutions.filename}:')
646
647     sys.exit(retval)
648
649 if __name__ == "__main__":
650     main()