]>
Commit | Line | Data |
---|---|---|
acddc0ed | 1 | # SPDX-License-Identifier: GPL-2.0-or-later |
5578a14d DL |
2 | # FRR CLI preprocessor (DEFPY) |
3 | # | |
4 | # Copyright (C) 2017 David Lamparter for NetDEF, Inc. | |
5578a14d DL |
5 | |
6 | import clippy, traceback, sys, os | |
7 | from collections import OrderedDict | |
8 | from functools import reduce | |
9 | from pprint import pprint | |
10 | from string import Template | |
11 | from io import StringIO | |
12 | ||
13 | # the various handlers generate output C code for a particular type of | |
14 | # CLI token, choosing the most useful output C type. | |
15 | ||
701a0192 | 16 | |
5578a14d DL |
17 | class RenderHandler(object): |
18 | def __init__(self, token): | |
19 | pass | |
701a0192 | 20 | |
5578a14d DL |
21 | def combine(self, other): |
22 | if type(self) == type(other): | |
23 | return other | |
24 | return StringHandler(None) | |
25 | ||
701a0192 | 26 | deref = "" |
5578a14d | 27 | drop_str = False |
0ee0892b | 28 | canfail = True |
4381a59b | 29 | canassert = False |
5578a14d | 30 | |
701a0192 | 31 | |
5578a14d | 32 | class StringHandler(RenderHandler): |
701a0192 | 33 | argtype = "const char *" |
34 | decl = Template("const char *$varname = NULL;") | |
35 | code = Template( | |
36 | "$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;" | |
37 | ) | |
5578a14d | 38 | drop_str = True |
0ee0892b | 39 | canfail = False |
4381a59b | 40 | canassert = True |
5578a14d | 41 | |
701a0192 | 42 | |
5578a14d | 43 | class LongHandler(RenderHandler): |
701a0192 | 44 | argtype = "long" |
45 | decl = Template("long $varname = 0;") | |
46 | code = Template( | |
47 | """\ | |
5578a14d DL |
48 | char *_end; |
49 | $varname = strtol(argv[_i]->arg, &_end, 10); | |
701a0192 | 50 | _fail = (_end == argv[_i]->arg) || (*_end != '\\0');""" |
51 | ) | |
52 | ||
5578a14d | 53 | |
8079a413 PG |
54 | class AsDotHandler(RenderHandler): |
55 | argtype = "as_t" | |
56 | decl = Template("as_t $varname = 0;") | |
57 | code = Template("_fail = !asn_str2asn(argv[_i]->arg, &$varname);") | |
58 | ||
59 | ||
5578a14d DL |
60 | # A.B.C.D/M (prefix_ipv4) and |
61 | # X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a | |
62 | # struct prefix: | |
63 | ||
701a0192 | 64 | |
5578a14d DL |
65 | class PrefixBase(RenderHandler): |
66 | def combine(self, other): | |
67 | if type(self) == type(other): | |
68 | return other | |
473196f6 | 69 | if isinstance(other, PrefixBase): |
5578a14d DL |
70 | return PrefixGenHandler(None) |
71 | return StringHandler(None) | |
701a0192 | 72 | |
73 | deref = "&" | |
74 | ||
75 | ||
5578a14d | 76 | class Prefix4Handler(PrefixBase): |
701a0192 | 77 | argtype = "const struct prefix_ipv4 *" |
78 | decl = Template("struct prefix_ipv4 $varname = { };") | |
79 | code = Template("_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);") | |
80 | ||
81 | ||
5578a14d | 82 | class Prefix6Handler(PrefixBase): |
701a0192 | 83 | argtype = "const struct prefix_ipv6 *" |
84 | decl = Template("struct prefix_ipv6 $varname = { };") | |
85 | code = Template("_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);") | |
86 | ||
87 | ||
473196f6 | 88 | class PrefixEthHandler(PrefixBase): |
701a0192 | 89 | argtype = "struct prefix_eth *" |
90 | decl = Template("struct prefix_eth $varname = { };") | |
91 | code = Template("_fail = !str2prefix_eth(argv[_i]->arg, &$varname);") | |
92 | ||
93 | ||
5578a14d | 94 | class PrefixGenHandler(PrefixBase): |
701a0192 | 95 | argtype = "const struct prefix *" |
96 | decl = Template("struct prefix $varname = { };") | |
97 | code = Template("_fail = !str2prefix(argv[_i]->arg, &$varname);") | |
98 | ||
5578a14d DL |
99 | |
100 | # same for IP addresses. result is union sockunion. | |
101 | class IPBase(RenderHandler): | |
102 | def combine(self, other): | |
103 | if type(self) == type(other): | |
104 | return other | |
105 | if type(other) in [IP4Handler, IP6Handler, IPGenHandler]: | |
106 | return IPGenHandler(None) | |
107 | return StringHandler(None) | |
701a0192 | 108 | |
109 | ||
5578a14d | 110 | class IP4Handler(IPBase): |
701a0192 | 111 | argtype = "struct in_addr" |
112 | decl = Template("struct in_addr $varname = { INADDR_ANY };") | |
113 | code = Template("_fail = !inet_aton(argv[_i]->arg, &$varname);") | |
114 | ||
115 | ||
5578a14d | 116 | class IP6Handler(IPBase): |
701a0192 | 117 | argtype = "struct in6_addr" |
118 | decl = Template("struct in6_addr $varname = {};") | |
119 | code = Template("_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);") | |
120 | ||
121 | ||
5578a14d | 122 | class IPGenHandler(IPBase): |
701a0192 | 123 | argtype = "const union sockunion *" |
124 | decl = Template( | |
125 | """union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;""" | |
126 | ) | |
127 | code = Template( | |
128 | """\ | |
5578a14d DL |
129 | if (argv[_i]->text[0] == 'X') { |
130 | s__$varname.sa.sa_family = AF_INET6; | |
131 | _fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr); | |
132 | $varname = &s__$varname; | |
133 | } else { | |
134 | s__$varname.sa.sa_family = AF_INET; | |
135 | _fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr); | |
136 | $varname = &s__$varname; | |
701a0192 | 137 | }""" |
138 | ) | |
4381a59b | 139 | canassert = True |
5578a14d | 140 | |
701a0192 | 141 | |
5578a14d DL |
142 | def mix_handlers(handlers): |
143 | def combine(a, b): | |
144 | if a is None: | |
145 | return b | |
146 | return a.combine(b) | |
701a0192 | 147 | |
5578a14d DL |
148 | return reduce(combine, handlers, None) |
149 | ||
701a0192 | 150 | |
5578a14d | 151 | handlers = { |
701a0192 | 152 | "WORD_TKN": StringHandler, |
153 | "VARIABLE_TKN": StringHandler, | |
154 | "RANGE_TKN": LongHandler, | |
155 | "IPV4_TKN": IP4Handler, | |
156 | "IPV4_PREFIX_TKN": Prefix4Handler, | |
157 | "IPV6_TKN": IP6Handler, | |
158 | "IPV6_PREFIX_TKN": Prefix6Handler, | |
159 | "MAC_TKN": PrefixEthHandler, | |
160 | "MAC_PREFIX_TKN": PrefixEthHandler, | |
8079a413 | 161 | "ASNUM_TKN": AsDotHandler, |
5578a14d DL |
162 | } |
163 | ||
164 | # core template invoked for each occurence of DEFPY. | |
0ee0892b DL |
165 | # |
166 | # the "#if $..." bits are there to keep this template unified into one | |
167 | # common form, without requiring a more advanced template engine (e.g. | |
168 | # jinja2) | |
701a0192 | 169 | templ = Template( |
d11238bb | 170 | """$cond_begin/* $fnname => "$cmddef" */ |
5578a14d DL |
171 | DEFUN_CMD_FUNC_DECL($fnname) |
172 | #define funcdecl_$fnname static int ${fnname}_magic(\\ | |
173 | const struct cmd_element *self __attribute__ ((unused)),\\ | |
174 | struct vty *vty __attribute__ ((unused)),\\ | |
175 | int argc __attribute__ ((unused)),\\ | |
176 | struct cmd_token *argv[] __attribute__ ((unused))$argdefs) | |
177 | funcdecl_$fnname; | |
178 | DEFUN_CMD_FUNC_TEXT($fnname) | |
179 | { | |
0ee0892b | 180 | #if $nonempty /* anything to parse? */ |
5578a14d | 181 | int _i; |
0ee0892b | 182 | #if $canfail /* anything that can fail? */ |
5578a14d | 183 | unsigned _fail = 0, _failcnt = 0; |
0ee0892b | 184 | #endif |
5578a14d DL |
185 | $argdecls |
186 | for (_i = 0; _i < argc; _i++) { | |
187 | if (!argv[_i]->varname) | |
188 | continue; | |
0ee0892b DL |
189 | #if $canfail /* anything that can fail? */ |
190 | _fail = 0; | |
191 | #endif | |
192 | $argblocks | |
193 | #if $canfail /* anything that can fail? */ | |
5578a14d | 194 | if (_fail) |
5c7571d4 | 195 | vty_out (vty, "%% invalid input for %s: %s\\n", |
8e25c8ce | 196 | argv[_i]->varname, argv[_i]->arg); |
5578a14d | 197 | _failcnt += _fail; |
0ee0892b | 198 | #endif |
5578a14d | 199 | } |
0ee0892b | 200 | #if $canfail /* anything that can fail? */ |
5578a14d DL |
201 | if (_failcnt) |
202 | return CMD_WARNING; | |
0ee0892b DL |
203 | #endif |
204 | #endif | |
4381a59b | 205 | $argassert |
5578a14d DL |
206 | return ${fnname}_magic(self, vty, argc, argv$arglist); |
207 | } | |
d11238bb | 208 | $cond_end |
701a0192 | 209 | """ |
210 | ) | |
5578a14d DL |
211 | |
212 | # invoked for each named parameter | |
701a0192 | 213 | argblock = Template( |
214 | """ | |
5578a14d DL |
215 | if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock |
216 | $code | |
701a0192 | 217 | }""" |
218 | ) | |
5578a14d | 219 | |
701a0192 | 220 | |
221 | def get_always_args(token, always_args, args=[], stack=[]): | |
4381a59b DL |
222 | if token in stack: |
223 | return | |
701a0192 | 224 | if token.type == "END_TKN": |
4381a59b DL |
225 | for arg in list(always_args): |
226 | if arg not in args: | |
227 | always_args.remove(arg) | |
228 | return | |
229 | ||
230 | stack = stack + [token] | |
231 | if token.type in handlers and token.varname is not None: | |
232 | args = args + [token.varname] | |
233 | for nexttkn in token.next(): | |
234 | get_always_args(nexttkn, always_args, args, stack) | |
235 | ||
701a0192 | 236 | |
b41b3f7b | 237 | class Macros(dict): |
e960eedd DL |
238 | def __init__(self): |
239 | super().__init__() | |
240 | self._loc = {} | |
241 | ||
b41b3f7b DL |
242 | def load(self, filename): |
243 | filedata = clippy.parse(filename) | |
701a0192 | 244 | for entry in filedata["data"]: |
245 | if entry["type"] != "PREPROC": | |
b41b3f7b | 246 | continue |
e960eedd DL |
247 | self.load_preproc(filename, entry) |
248 | ||
249 | def setup(self, key, val, where="built-in"): | |
250 | self[key] = val | |
251 | self._loc[key] = (where, 0) | |
252 | ||
253 | def load_preproc(self, filename, entry): | |
254 | ppdir = entry["line"].lstrip().split(None, 1) | |
255 | if ppdir[0] != "define" or len(ppdir) != 2: | |
256 | return | |
257 | ppdef = ppdir[1].split(None, 1) | |
258 | name = ppdef[0] | |
259 | if "(" in name: | |
260 | return | |
261 | val = ppdef[1] if len(ppdef) == 2 else "" | |
262 | ||
263 | val = val.strip(" \t\n\\") | |
264 | if self.get(name, val) != val: | |
265 | sys.stderr.write( | |
266 | "%s:%d: warning: macro %s redefined!\n" | |
267 | % ( | |
268 | filename, | |
269 | entry["lineno"], | |
270 | name, | |
271 | ) | |
272 | ) | |
273 | sys.stderr.write( | |
274 | "%s:%d: note: previously defined here\n" | |
275 | % ( | |
276 | self._loc[name][0], | |
277 | self._loc[name][1], | |
278 | ) | |
279 | ) | |
280 | else: | |
b41b3f7b | 281 | self[name] = val |
e960eedd | 282 | self._loc[name] = (filename, entry["lineno"]) |
b41b3f7b | 283 | |
701a0192 | 284 | |
b41b3f7b | 285 | def process_file(fn, ofd, dumpfd, all_defun, macros): |
3779776a | 286 | errors = 0 |
5578a14d DL |
287 | filedata = clippy.parse(fn) |
288 | ||
d11238bb DL |
289 | cond_stack = [] |
290 | ||
701a0192 | 291 | for entry in filedata["data"]: |
d11238bb DL |
292 | if entry["type"] == "PREPROC": |
293 | line = entry["line"].lstrip() | |
294 | tokens = line.split(maxsplit=1) | |
295 | line = "#" + line + "\n" | |
296 | ||
297 | if not tokens: | |
298 | continue | |
299 | ||
300 | if tokens[0] in ["if", "ifdef", "ifndef"]: | |
301 | cond_stack.append(line) | |
302 | elif tokens[0] in ["elif", "else"]: | |
303 | prev_line = cond_stack.pop(-1) | |
304 | cond_stack.append(prev_line + line) | |
305 | elif tokens[0] in ["endif"]: | |
306 | cond_stack.pop(-1) | |
e960eedd DL |
307 | elif tokens[0] in ["define"]: |
308 | if not cond_stack: | |
309 | macros.load_preproc(fn, entry) | |
310 | elif len(cond_stack) == 1 and cond_stack[0] == "#ifdef CLIPPY\n": | |
311 | macros.load_preproc(fn, entry) | |
d11238bb | 312 | continue |
701a0192 | 313 | if entry["type"].startswith("DEFPY") or ( |
314 | all_defun and entry["type"].startswith("DEFUN") | |
315 | ): | |
316 | if len(entry["args"][0]) != 1: | |
317 | sys.stderr.write( | |
318 | "%s:%d: DEFPY function name not parseable (%r)\n" | |
319 | % (fn, entry["lineno"], entry["args"][0]) | |
320 | ) | |
3779776a DL |
321 | errors += 1 |
322 | continue | |
323 | ||
701a0192 | 324 | cmddef = entry["args"][2] |
b41b3f7b | 325 | cmddefx = [] |
3779776a | 326 | for i in cmddef: |
b41b3f7b DL |
327 | while i in macros: |
328 | i = macros[i] | |
329 | if i.startswith('"') and i.endswith('"'): | |
330 | cmddefx.append(i[1:-1]) | |
331 | continue | |
332 | ||
701a0192 | 333 | sys.stderr.write( |
334 | "%s:%d: DEFPY command string not parseable (%r)\n" | |
335 | % (fn, entry["lineno"], cmddef) | |
336 | ) | |
b41b3f7b DL |
337 | errors += 1 |
338 | cmddefx = None | |
339 | break | |
340 | if cmddefx is None: | |
3779776a | 341 | continue |
701a0192 | 342 | cmddef = "".join([i for i in cmddefx]) |
5578a14d DL |
343 | |
344 | graph = clippy.Graph(cmddef) | |
345 | args = OrderedDict() | |
4381a59b | 346 | always_args = set() |
5578a14d DL |
347 | for token, depth in clippy.graph_iterate(graph): |
348 | if token.type not in handlers: | |
349 | continue | |
350 | if token.varname is None: | |
351 | continue | |
352 | arg = args.setdefault(token.varname, []) | |
353 | arg.append(handlers[token.type](token)) | |
4381a59b DL |
354 | always_args.add(token.varname) |
355 | ||
356 | get_always_args(graph.first(), always_args) | |
5578a14d | 357 | |
701a0192 | 358 | # print('-' * 76) |
359 | # pprint(entry) | |
360 | # clippy.dump(graph) | |
361 | # pprint(args) | |
5578a14d | 362 | |
701a0192 | 363 | params = {"cmddef": cmddef, "fnname": entry["args"][0][0]} |
5578a14d DL |
364 | argdefs = [] |
365 | argdecls = [] | |
366 | arglist = [] | |
367 | argblocks = [] | |
4381a59b | 368 | argassert = [] |
5578a14d | 369 | doc = [] |
0ee0892b | 370 | canfail = 0 |
5578a14d | 371 | |
701a0192 | 372 | def do_add(handler, basename, varname, attr=""): |
373 | argdefs.append(",\\\n\t%s %s%s" % (handler.argtype, varname, attr)) | |
374 | argdecls.append( | |
375 | "\t%s\n" | |
376 | % ( | |
377 | handler.decl.substitute({"varname": varname}).replace( | |
378 | "\n", "\n\t" | |
379 | ) | |
380 | ) | |
381 | ) | |
382 | arglist.append(", %s%s" % (handler.deref, varname)) | |
4381a59b | 383 | if basename in always_args and handler.canassert: |
701a0192 | 384 | argassert.append( |
385 | """\tif (!%s) { | |
4381a59b DL |
386 | \t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s"); |
387 | \t\treturn CMD_WARNING; | |
701a0192 | 388 | \t}\n""" |
389 | % (varname, varname) | |
390 | ) | |
391 | if attr == "": | |
5578a14d | 392 | at = handler.argtype |
701a0192 | 393 | if not at.startswith("const "): |
394 | at = ". . . " + at | |
395 | doc.append( | |
396 | "\t%-26s %s %s" | |
397 | % (at, "alw" if basename in always_args else "opt", varname) | |
398 | ) | |
5578a14d DL |
399 | |
400 | for varname in args.keys(): | |
401 | handler = mix_handlers(args[varname]) | |
701a0192 | 402 | # print(varname, handler) |
403 | if handler is None: | |
404 | continue | |
4381a59b | 405 | do_add(handler, varname, varname) |
701a0192 | 406 | code = handler.code.substitute({"varname": varname}).replace( |
407 | "\n", "\n\t\t\t" | |
408 | ) | |
0ee0892b DL |
409 | if handler.canfail: |
410 | canfail = 1 | |
701a0192 | 411 | strblock = "" |
5578a14d | 412 | if not handler.drop_str: |
701a0192 | 413 | do_add( |
414 | StringHandler(None), | |
415 | varname, | |
416 | "%s_str" % (varname), | |
417 | " __attribute__ ((unused))", | |
418 | ) | |
419 | strblock = "\n\t\t\t%s_str = argv[_i]->arg;" % (varname) | |
420 | argblocks.append( | |
421 | argblock.substitute( | |
422 | {"varname": varname, "strblock": strblock, "code": code} | |
423 | ) | |
424 | ) | |
5578a14d DL |
425 | |
426 | if dumpfd is not None: | |
427 | if len(arglist) > 0: | |
701a0192 | 428 | dumpfd.write('"%s":\n%s\n\n' % (cmddef, "\n".join(doc))) |
5578a14d DL |
429 | else: |
430 | dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef)) | |
431 | ||
d11238bb DL |
432 | params["cond_begin"] = "".join(cond_stack) |
433 | params["cond_end"] = "".join(["#endif\n"] * len(cond_stack)) | |
701a0192 | 434 | params["argdefs"] = "".join(argdefs) |
435 | params["argdecls"] = "".join(argdecls) | |
436 | params["arglist"] = "".join(arglist) | |
437 | params["argblocks"] = "".join(argblocks) | |
438 | params["canfail"] = canfail | |
439 | params["nonempty"] = len(argblocks) | |
440 | params["argassert"] = "".join(argassert) | |
5578a14d DL |
441 | ofd.write(templ.substitute(params)) |
442 | ||
3779776a DL |
443 | return errors |
444 | ||
701a0192 | 445 | |
446 | if __name__ == "__main__": | |
5578a14d DL |
447 | import argparse |
448 | ||
701a0192 | 449 | argp = argparse.ArgumentParser(description="FRR CLI preprocessor in Python") |
450 | argp.add_argument( | |
451 | "--all-defun", | |
452 | action="store_const", | |
453 | const=True, | |
454 | help="process DEFUN() statements in addition to DEFPY()", | |
455 | ) | |
456 | argp.add_argument( | |
457 | "--show", | |
458 | action="store_const", | |
459 | const=True, | |
460 | help="print out list of arguments and types for each definition", | |
461 | ) | |
462 | argp.add_argument("-o", type=str, metavar="OUTFILE", help="output C file name") | |
463 | argp.add_argument("cfile", type=str) | |
5578a14d DL |
464 | args = argp.parse_args() |
465 | ||
466 | dumpfd = None | |
467 | if args.o is not None: | |
468 | ofd = StringIO() | |
469 | if args.show: | |
470 | dumpfd = sys.stdout | |
471 | else: | |
472 | ofd = sys.stdout | |
473 | if args.show: | |
474 | dumpfd = sys.stderr | |
475 | ||
c4ca3ef5 DL |
476 | basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
477 | ||
b41b3f7b | 478 | macros = Macros() |
701a0192 | 479 | macros.load("lib/route_types.h") |
480 | macros.load(os.path.join(basepath, "lib/command.h")) | |
481 | macros.load(os.path.join(basepath, "bgpd/bgp_vty.h")) | |
b41b3f7b | 482 | # sigh :( |
e960eedd DL |
483 | macros.setup("PROTO_REDIST_STR", "FRR_REDIST_STR_ISISD") |
484 | macros.setup("PROTO_IP_REDIST_STR", "FRR_IP_REDIST_STR_ISISD") | |
485 | macros.setup("PROTO_IP6_REDIST_STR", "FRR_IP6_REDIST_STR_ISISD") | |
b41b3f7b DL |
486 | |
487 | errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros) | |
3779776a DL |
488 | if errors != 0: |
489 | sys.exit(1) | |
5578a14d DL |
490 | |
491 | if args.o is not None: | |
701a0192 | 492 | clippy.wrdiff( |
493 | args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable] | |
494 | ) |