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