]> git.proxmox.com Git - mirror_frr.git/blob - python/clidef.py
Merge pull request #7162 from opensourcerouting/zebra-human-netlink
[mirror_frr.git] / python / clidef.py
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
29
30 class RenderHandler(object):
31 def __init__(self, token):
32 pass
33
34 def combine(self, other):
35 if type(self) == type(other):
36 return other
37 return StringHandler(None)
38
39 deref = ""
40 drop_str = False
41 canfail = True
42 canassert = False
43
44
45 class StringHandler(RenderHandler):
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 )
51 drop_str = True
52 canfail = False
53 canassert = True
54
55
56 class LongHandler(RenderHandler):
57 argtype = "long"
58 decl = Template("long $varname = 0;")
59 code = Template(
60 """\
61 char *_end;
62 $varname = strtol(argv[_i]->arg, &_end, 10);
63 _fail = (_end == argv[_i]->arg) || (*_end != '\\0');"""
64 )
65
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
71
72 class PrefixBase(RenderHandler):
73 def combine(self, other):
74 if type(self) == type(other):
75 return other
76 if isinstance(other, PrefixBase):
77 return PrefixGenHandler(None)
78 return StringHandler(None)
79
80 deref = "&"
81
82
83 class Prefix4Handler(PrefixBase):
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
89 class Prefix6Handler(PrefixBase):
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
95 class PrefixEthHandler(PrefixBase):
96 argtype = "struct prefix_eth *"
97 decl = Template("struct prefix_eth $varname = { };")
98 code = Template("_fail = !str2prefix_eth(argv[_i]->arg, &$varname);")
99
100
101 class PrefixGenHandler(PrefixBase):
102 argtype = "const struct prefix *"
103 decl = Template("struct prefix $varname = { };")
104 code = Template("_fail = !str2prefix(argv[_i]->arg, &$varname);")
105
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)
115
116
117 class IP4Handler(IPBase):
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
123 class IP6Handler(IPBase):
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
129 class IPGenHandler(IPBase):
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 """\
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;
144 }"""
145 )
146 canassert = True
147
148
149 def mix_handlers(handlers):
150 def combine(a, b):
151 if a is None:
152 return b
153 return a.combine(b)
154
155 return reduce(combine, handlers, None)
156
157
158 handlers = {
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,
168 }
169
170 # core template invoked for each occurence of DEFPY.
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)
175 templ = Template(
176 """/* $fnname => "$cmddef" */
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 {
186 #if $nonempty /* anything to parse? */
187 int _i;
188 #if $canfail /* anything that can fail? */
189 unsigned _fail = 0, _failcnt = 0;
190 #endif
191 $argdecls
192 for (_i = 0; _i < argc; _i++) {
193 if (!argv[_i]->varname)
194 continue;
195 #if $canfail /* anything that can fail? */
196 _fail = 0;
197 #endif
198 $argblocks
199 #if $canfail /* anything that can fail? */
200 if (_fail)
201 vty_out (vty, "%% invalid input for %s: %s\\n",
202 argv[_i]->varname, argv[_i]->arg);
203 _failcnt += _fail;
204 #endif
205 }
206 #if $canfail /* anything that can fail? */
207 if (_failcnt)
208 return CMD_WARNING;
209 #endif
210 #endif
211 $argassert
212 return ${fnname}_magic(self, vty, argc, argv$arglist);
213 }
214
215 """
216 )
217
218 # invoked for each named parameter
219 argblock = Template(
220 """
221 if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock
222 $code
223 }"""
224 )
225
226
227 def get_always_args(token, always_args, args=[], stack=[]):
228 if token in stack:
229 return
230 if token.type == "END_TKN":
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
242
243 class Macros(dict):
244 def load(self, filename):
245 filedata = clippy.parse(filename)
246 for entry in filedata["data"]:
247 if entry["type"] != "PREPROC":
248 continue
249 ppdir = entry["line"].lstrip().split(None, 1)
250 if ppdir[0] != "define" or len(ppdir) != 2:
251 continue
252 ppdef = ppdir[1].split(None, 1)
253 name = ppdef[0]
254 if "(" in name:
255 continue
256 val = ppdef[1] if len(ppdef) == 2 else ""
257
258 val = val.strip(" \t\n\\")
259 if name in self:
260 sys.stderr.write("warning: macro %s redefined!\n" % (name))
261 self[name] = val
262
263
264 def process_file(fn, ofd, dumpfd, all_defun, macros):
265 errors = 0
266 filedata = clippy.parse(fn)
267
268 for entry in filedata["data"]:
269 if entry["type"].startswith("DEFPY") or (
270 all_defun and entry["type"].startswith("DEFUN")
271 ):
272 if len(entry["args"][0]) != 1:
273 sys.stderr.write(
274 "%s:%d: DEFPY function name not parseable (%r)\n"
275 % (fn, entry["lineno"], entry["args"][0])
276 )
277 errors += 1
278 continue
279
280 cmddef = entry["args"][2]
281 cmddefx = []
282 for i in cmddef:
283 while i in macros:
284 i = macros[i]
285 if i.startswith('"') and i.endswith('"'):
286 cmddefx.append(i[1:-1])
287 continue
288
289 sys.stderr.write(
290 "%s:%d: DEFPY command string not parseable (%r)\n"
291 % (fn, entry["lineno"], cmddef)
292 )
293 errors += 1
294 cmddefx = None
295 break
296 if cmddefx is None:
297 continue
298 cmddef = "".join([i for i in cmddefx])
299
300 graph = clippy.Graph(cmddef)
301 args = OrderedDict()
302 always_args = set()
303 for token, depth in clippy.graph_iterate(graph):
304 if token.type not in handlers:
305 continue
306 if token.varname is None:
307 continue
308 arg = args.setdefault(token.varname, [])
309 arg.append(handlers[token.type](token))
310 always_args.add(token.varname)
311
312 get_always_args(graph.first(), always_args)
313
314 # print('-' * 76)
315 # pprint(entry)
316 # clippy.dump(graph)
317 # pprint(args)
318
319 params = {"cmddef": cmddef, "fnname": entry["args"][0][0]}
320 argdefs = []
321 argdecls = []
322 arglist = []
323 argblocks = []
324 argassert = []
325 doc = []
326 canfail = 0
327
328 def do_add(handler, basename, varname, attr=""):
329 argdefs.append(",\\\n\t%s %s%s" % (handler.argtype, varname, attr))
330 argdecls.append(
331 "\t%s\n"
332 % (
333 handler.decl.substitute({"varname": varname}).replace(
334 "\n", "\n\t"
335 )
336 )
337 )
338 arglist.append(", %s%s" % (handler.deref, varname))
339 if basename in always_args and handler.canassert:
340 argassert.append(
341 """\tif (!%s) {
342 \t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s");
343 \t\treturn CMD_WARNING;
344 \t}\n"""
345 % (varname, varname)
346 )
347 if attr == "":
348 at = handler.argtype
349 if not at.startswith("const "):
350 at = ". . . " + at
351 doc.append(
352 "\t%-26s %s %s"
353 % (at, "alw" if basename in always_args else "opt", varname)
354 )
355
356 for varname in args.keys():
357 handler = mix_handlers(args[varname])
358 # print(varname, handler)
359 if handler is None:
360 continue
361 do_add(handler, varname, varname)
362 code = handler.code.substitute({"varname": varname}).replace(
363 "\n", "\n\t\t\t"
364 )
365 if handler.canfail:
366 canfail = 1
367 strblock = ""
368 if not handler.drop_str:
369 do_add(
370 StringHandler(None),
371 varname,
372 "%s_str" % (varname),
373 " __attribute__ ((unused))",
374 )
375 strblock = "\n\t\t\t%s_str = argv[_i]->arg;" % (varname)
376 argblocks.append(
377 argblock.substitute(
378 {"varname": varname, "strblock": strblock, "code": code}
379 )
380 )
381
382 if dumpfd is not None:
383 if len(arglist) > 0:
384 dumpfd.write('"%s":\n%s\n\n' % (cmddef, "\n".join(doc)))
385 else:
386 dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef))
387
388 params["argdefs"] = "".join(argdefs)
389 params["argdecls"] = "".join(argdecls)
390 params["arglist"] = "".join(arglist)
391 params["argblocks"] = "".join(argblocks)
392 params["canfail"] = canfail
393 params["nonempty"] = len(argblocks)
394 params["argassert"] = "".join(argassert)
395 ofd.write(templ.substitute(params))
396
397 return errors
398
399
400 if __name__ == "__main__":
401 import argparse
402
403 argp = argparse.ArgumentParser(description="FRR CLI preprocessor in Python")
404 argp.add_argument(
405 "--all-defun",
406 action="store_const",
407 const=True,
408 help="process DEFUN() statements in addition to DEFPY()",
409 )
410 argp.add_argument(
411 "--show",
412 action="store_const",
413 const=True,
414 help="print out list of arguments and types for each definition",
415 )
416 argp.add_argument("-o", type=str, metavar="OUTFILE", help="output C file name")
417 argp.add_argument("cfile", type=str)
418 args = argp.parse_args()
419
420 dumpfd = None
421 if args.o is not None:
422 ofd = StringIO()
423 if args.show:
424 dumpfd = sys.stdout
425 else:
426 ofd = sys.stdout
427 if args.show:
428 dumpfd = sys.stderr
429
430 basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
431
432 macros = Macros()
433 macros.load("lib/route_types.h")
434 macros.load(os.path.join(basepath, "lib/command.h"))
435 macros.load(os.path.join(basepath, "bgpd/bgp_vty.h"))
436 # sigh :(
437 macros["PROTO_REDIST_STR"] = "FRR_REDIST_STR_ISISD"
438
439 errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros)
440 if errors != 0:
441 sys.exit(1)
442
443 if args.o is not None:
444 clippy.wrdiff(
445 args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable]
446 )