diff options
Diffstat (limited to '')
-rw-r--r-- | python/clidef.py | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/python/clidef.py b/python/clidef.py new file mode 100644 index 0000000..244a820 --- /dev/null +++ b/python/clidef.py @@ -0,0 +1,494 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# FRR CLI preprocessor (DEFPY) +# +# Copyright (C) 2017 David Lamparter for NetDEF, Inc. + +import clippy, traceback, sys, os +from collections import OrderedDict +from functools import reduce +from pprint import pprint +from string import Template +from io import StringIO + +# the various handlers generate output C code for a particular type of +# CLI token, choosing the most useful output C type. + + +class RenderHandler(object): + def __init__(self, token): + pass + + def combine(self, other): + if type(self) == type(other): + return other + return StringHandler(None) + + deref = "" + drop_str = False + canfail = True + canassert = False + + +class StringHandler(RenderHandler): + argtype = "const char *" + decl = Template("const char *$varname = NULL;") + code = Template( + "$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;" + ) + drop_str = True + canfail = False + canassert = True + + +class LongHandler(RenderHandler): + argtype = "long" + decl = Template("long $varname = 0;") + code = Template( + """\ +char *_end; +$varname = strtol(argv[_i]->arg, &_end, 10); +_fail = (_end == argv[_i]->arg) || (*_end != '\\0');""" + ) + + +class AsDotHandler(RenderHandler): + argtype = "as_t" + decl = Template("as_t $varname = 0;") + code = Template("_fail = !asn_str2asn(argv[_i]->arg, &$varname);") + + +# A.B.C.D/M (prefix_ipv4) and +# X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a +# struct prefix: + + +class PrefixBase(RenderHandler): + def combine(self, other): + if type(self) == type(other): + return other + if isinstance(other, PrefixBase): + return PrefixGenHandler(None) + return StringHandler(None) + + deref = "&" + + +class Prefix4Handler(PrefixBase): + argtype = "const struct prefix_ipv4 *" + decl = Template("struct prefix_ipv4 $varname = { };") + code = Template("_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);") + + +class Prefix6Handler(PrefixBase): + argtype = "const struct prefix_ipv6 *" + decl = Template("struct prefix_ipv6 $varname = { };") + code = Template("_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);") + + +class PrefixEthHandler(PrefixBase): + argtype = "struct prefix_eth *" + decl = Template("struct prefix_eth $varname = { };") + code = Template("_fail = !str2prefix_eth(argv[_i]->arg, &$varname);") + + +class PrefixGenHandler(PrefixBase): + argtype = "const struct prefix *" + decl = Template("struct prefix $varname = { };") + code = Template("_fail = !str2prefix(argv[_i]->arg, &$varname);") + + +# same for IP addresses. result is union sockunion. +class IPBase(RenderHandler): + def combine(self, other): + if type(self) == type(other): + return other + if type(other) in [IP4Handler, IP6Handler, IPGenHandler]: + return IPGenHandler(None) + return StringHandler(None) + + +class IP4Handler(IPBase): + argtype = "struct in_addr" + decl = Template("struct in_addr $varname = { INADDR_ANY };") + code = Template("_fail = !inet_aton(argv[_i]->arg, &$varname);") + + +class IP6Handler(IPBase): + argtype = "struct in6_addr" + decl = Template("struct in6_addr $varname = {};") + code = Template("_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);") + + +class IPGenHandler(IPBase): + argtype = "const union sockunion *" + decl = Template( + """union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;""" + ) + code = Template( + """\ +if (argv[_i]->text[0] == 'X') { + s__$varname.sa.sa_family = AF_INET6; + _fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr); + $varname = &s__$varname; +} else { + s__$varname.sa.sa_family = AF_INET; + _fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr); + $varname = &s__$varname; +}""" + ) + canassert = True + + +def mix_handlers(handlers): + def combine(a, b): + if a is None: + return b + return a.combine(b) + + return reduce(combine, handlers, None) + + +handlers = { + "WORD_TKN": StringHandler, + "VARIABLE_TKN": StringHandler, + "RANGE_TKN": LongHandler, + "IPV4_TKN": IP4Handler, + "IPV4_PREFIX_TKN": Prefix4Handler, + "IPV6_TKN": IP6Handler, + "IPV6_PREFIX_TKN": Prefix6Handler, + "MAC_TKN": PrefixEthHandler, + "MAC_PREFIX_TKN": PrefixEthHandler, + "ASNUM_TKN": AsDotHandler, +} + +# core template invoked for each occurence of DEFPY. +# +# the "#if $..." bits are there to keep this template unified into one +# common form, without requiring a more advanced template engine (e.g. +# jinja2) +templ = Template( + """$cond_begin/* $fnname => "$cmddef" */ +DEFUN_CMD_FUNC_DECL($fnname) +#define funcdecl_$fnname static int ${fnname}_magic(\\ + const struct cmd_element *self __attribute__ ((unused)),\\ + struct vty *vty __attribute__ ((unused)),\\ + int argc __attribute__ ((unused)),\\ + struct cmd_token *argv[] __attribute__ ((unused))$argdefs) +funcdecl_$fnname; +DEFUN_CMD_FUNC_TEXT($fnname) +{ +#if $nonempty /* anything to parse? */ + int _i; +#if $canfail /* anything that can fail? */ + unsigned _fail = 0, _failcnt = 0; +#endif +$argdecls + for (_i = 0; _i < argc; _i++) { + if (!argv[_i]->varname) + continue; +#if $canfail /* anything that can fail? */ + _fail = 0; +#endif +$argblocks +#if $canfail /* anything that can fail? */ + if (_fail) + vty_out (vty, "%% invalid input for %s: %s\\n", + argv[_i]->varname, argv[_i]->arg); + _failcnt += _fail; +#endif + } +#if $canfail /* anything that can fail? */ + if (_failcnt) + return CMD_WARNING; +#endif +#endif +$argassert + return ${fnname}_magic(self, vty, argc, argv$arglist); +} +$cond_end +""" +) + +# invoked for each named parameter +argblock = Template( + """ + if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock + $code + }""" +) + + +def get_always_args(token, always_args, args=[], stack=[]): + if token in stack: + return + if token.type == "END_TKN": + for arg in list(always_args): + if arg not in args: + always_args.remove(arg) + return + + stack = stack + [token] + if token.type in handlers and token.varname is not None: + args = args + [token.varname] + for nexttkn in token.next(): + get_always_args(nexttkn, always_args, args, stack) + + +class Macros(dict): + def __init__(self): + super().__init__() + self._loc = {} + + def load(self, filename): + filedata = clippy.parse(filename) + for entry in filedata["data"]: + if entry["type"] != "PREPROC": + continue + self.load_preproc(filename, entry) + + def setup(self, key, val, where="built-in"): + self[key] = val + self._loc[key] = (where, 0) + + def load_preproc(self, filename, entry): + ppdir = entry["line"].lstrip().split(None, 1) + if ppdir[0] != "define" or len(ppdir) != 2: + return + ppdef = ppdir[1].split(None, 1) + name = ppdef[0] + if "(" in name: + return + val = ppdef[1] if len(ppdef) == 2 else "" + + val = val.strip(" \t\n\\") + if self.get(name, val) != val: + sys.stderr.write( + "%s:%d: warning: macro %s redefined!\n" + % ( + filename, + entry["lineno"], + name, + ) + ) + sys.stderr.write( + "%s:%d: note: previously defined here\n" + % ( + self._loc[name][0], + self._loc[name][1], + ) + ) + else: + self[name] = val + self._loc[name] = (filename, entry["lineno"]) + + +def process_file(fn, ofd, dumpfd, all_defun, macros): + errors = 0 + filedata = clippy.parse(fn) + + cond_stack = [] + + for entry in filedata["data"]: + if entry["type"] == "PREPROC": + line = entry["line"].lstrip() + tokens = line.split(maxsplit=1) + line = "#" + line + "\n" + + if not tokens: + continue + + if tokens[0] in ["if", "ifdef", "ifndef"]: + cond_stack.append(line) + elif tokens[0] in ["elif", "else"]: + prev_line = cond_stack.pop(-1) + cond_stack.append(prev_line + line) + elif tokens[0] in ["endif"]: + cond_stack.pop(-1) + elif tokens[0] in ["define"]: + if not cond_stack: + macros.load_preproc(fn, entry) + elif len(cond_stack) == 1 and cond_stack[0] == "#ifdef CLIPPY\n": + macros.load_preproc(fn, entry) + continue + if entry["type"].startswith("DEFPY") or ( + all_defun and entry["type"].startswith("DEFUN") + ): + if len(entry["args"][0]) != 1: + sys.stderr.write( + "%s:%d: DEFPY function name not parseable (%r)\n" + % (fn, entry["lineno"], entry["args"][0]) + ) + errors += 1 + continue + + cmddef = entry["args"][2] + cmddefx = [] + for i in cmddef: + while i in macros: + i = macros[i] + if i.startswith('"') and i.endswith('"'): + cmddefx.append(i[1:-1]) + continue + + sys.stderr.write( + "%s:%d: DEFPY command string not parseable (%r)\n" + % (fn, entry["lineno"], cmddef) + ) + errors += 1 + cmddefx = None + break + if cmddefx is None: + continue + cmddef = "".join([i for i in cmddefx]) + + graph = clippy.Graph(cmddef) + args = OrderedDict() + always_args = set() + for token, depth in clippy.graph_iterate(graph): + if token.type not in handlers: + continue + if token.varname is None: + continue + arg = args.setdefault(token.varname, []) + arg.append(handlers[token.type](token)) + always_args.add(token.varname) + + get_always_args(graph.first(), always_args) + + # print('-' * 76) + # pprint(entry) + # clippy.dump(graph) + # pprint(args) + + params = {"cmddef": cmddef, "fnname": entry["args"][0][0]} + argdefs = [] + argdecls = [] + arglist = [] + argblocks = [] + argassert = [] + doc = [] + canfail = 0 + + def do_add(handler, basename, varname, attr=""): + argdefs.append(",\\\n\t%s %s%s" % (handler.argtype, varname, attr)) + argdecls.append( + "\t%s\n" + % ( + handler.decl.substitute({"varname": varname}).replace( + "\n", "\n\t" + ) + ) + ) + arglist.append(", %s%s" % (handler.deref, varname)) + if basename in always_args and handler.canassert: + argassert.append( + """\tif (!%s) { +\t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s"); +\t\treturn CMD_WARNING; +\t}\n""" + % (varname, varname) + ) + if attr == "": + at = handler.argtype + if not at.startswith("const "): + at = ". . . " + at + doc.append( + "\t%-26s %s %s" + % (at, "alw" if basename in always_args else "opt", varname) + ) + + for varname in args.keys(): + handler = mix_handlers(args[varname]) + # print(varname, handler) + if handler is None: + continue + do_add(handler, varname, varname) + code = handler.code.substitute({"varname": varname}).replace( + "\n", "\n\t\t\t" + ) + if handler.canfail: + canfail = 1 + strblock = "" + if not handler.drop_str: + do_add( + StringHandler(None), + varname, + "%s_str" % (varname), + " __attribute__ ((unused))", + ) + strblock = "\n\t\t\t%s_str = argv[_i]->arg;" % (varname) + argblocks.append( + argblock.substitute( + {"varname": varname, "strblock": strblock, "code": code} + ) + ) + + if dumpfd is not None: + if len(arglist) > 0: + dumpfd.write('"%s":\n%s\n\n' % (cmddef, "\n".join(doc))) + else: + dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef)) + + params["cond_begin"] = "".join(cond_stack) + params["cond_end"] = "".join(["#endif\n"] * len(cond_stack)) + params["argdefs"] = "".join(argdefs) + params["argdecls"] = "".join(argdecls) + params["arglist"] = "".join(arglist) + params["argblocks"] = "".join(argblocks) + params["canfail"] = canfail + params["nonempty"] = len(argblocks) + params["argassert"] = "".join(argassert) + ofd.write(templ.substitute(params)) + + return errors + + +if __name__ == "__main__": + import argparse + + argp = argparse.ArgumentParser(description="FRR CLI preprocessor in Python") + argp.add_argument( + "--all-defun", + action="store_const", + const=True, + help="process DEFUN() statements in addition to DEFPY()", + ) + argp.add_argument( + "--show", + action="store_const", + const=True, + help="print out list of arguments and types for each definition", + ) + argp.add_argument("-o", type=str, metavar="OUTFILE", help="output C file name") + argp.add_argument("cfile", type=str) + args = argp.parse_args() + + dumpfd = None + if args.o is not None: + ofd = StringIO() + if args.show: + dumpfd = sys.stdout + else: + ofd = sys.stdout + if args.show: + dumpfd = sys.stderr + + basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + macros = Macros() + macros.load("lib/route_types.h") + macros.load(os.path.join(basepath, "lib/command.h")) + macros.load(os.path.join(basepath, "bgpd/bgp_vty.h")) + # sigh :( + macros.setup("PROTO_REDIST_STR", "FRR_REDIST_STR_ISISD") + macros.setup("PROTO_IP_REDIST_STR", "FRR_IP_REDIST_STR_ISISD") + macros.setup("PROTO_IP6_REDIST_STR", "FRR_IP6_REDIST_STR_ISISD") + + errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros) + if errors != 0: + sys.exit(1) + + if args.o is not None: + clippy.wrdiff( + args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable] + ) |