]> git.proxmox.com Git - mirror_frr.git/blame - python/clidef.py
Merge pull request #13649 from donaldsharp/unlock_the_node_or_else
[mirror_frr.git] / python / clidef.py
CommitLineData
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
6import clippy, traceback, sys, os
7from collections import OrderedDict
8from functools import reduce
9from pprint import pprint
10from string import Template
11from 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
17class 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 32class 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 43class LongHandler(RenderHandler):
701a0192 44 argtype = "long"
45 decl = Template("long $varname = 0;")
46 code = Template(
47 """\
5578a14d
DL
48char *_end;
49$varname = strtol(argv[_i]->arg, &_end, 10);
701a0192 50_fail = (_end == argv[_i]->arg) || (*_end != '\\0');"""
51 )
52
5578a14d 53
8079a413
PG
54class 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
65class 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 76class 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 82class 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 88class 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 94class 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.
101class 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 110class 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 116class 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 122class 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
129if (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
142def 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 151handlers = {
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 169templ = Template(
d11238bb 170 """$cond_begin/* $fnname => "$cmddef" */
5578a14d
DL
171DEFUN_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)
177funcdecl_$fnname;
178DEFUN_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 213argblock = Template(
214 """
5578a14d
DL
215 if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock
216 $code
701a0192 217 }"""
218)
5578a14d 219
701a0192 220
221def 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 237class 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 285def 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
446if __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 )