# FRR CLI preprocessor (DEFPY) # # Copyright (C) 2017 David Lamparter for NetDEF, Inc. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation; either version 2 of the License, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along # with this program; see the file COPYING; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 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');""" ) # 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, } # 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( """/* $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); } """ ) # 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 load(self, filename): filedata = clippy.parse(filename) for entry in filedata["data"]: if entry["type"] != "PREPROC": continue ppdir = entry["line"].lstrip().split(None, 1) if ppdir[0] != "define" or len(ppdir) != 2: continue ppdef = ppdir[1].split(None, 1) name = ppdef[0] if "(" in name: continue val = ppdef[1] if len(ppdef) == 2 else "" val = val.strip(" \t\n\\") if name in self: sys.stderr.write("warning: macro %s redefined!\n" % (name)) self[name] = val def process_file(fn, ofd, dumpfd, all_defun, macros): errors = 0 filedata = clippy.parse(fn) for entry in filedata["data"]: 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["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["PROTO_REDIST_STR"] = "FRR_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] )