mirror of
				https://git.proxmox.com/git/mirror_frr
				synced 2025-11-04 10:07:04 +00:00 
			
		
		
		
	Process macros from the current file, and warn if something is redefined (to a different value). Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
		
			
				
	
	
		
			501 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			501 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# 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(
 | 
						|
    """$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]
 | 
						|
        )
 |