diff options
| author | David Lamparter <equinox@opensourcerouting.org> | 2017-05-11 15:36:04 +0200 | 
|---|---|---|
| committer | David Lamparter <equinox@opensourcerouting.org> | 2017-06-14 19:29:26 +0200 | 
| commit | 5578a14d949d89e25ec3e6136158603049e5a2dd (patch) | |
| tree | fc1df6006271fb7f73cf8802c4022b266e0344b1 /python | |
| parent | 29ad6f6882de5a871d6d81c609e8b20d825d6138 (diff) | |
python: clidef.py
Adds "DEFPY()" which invokes an additional layer of preprocessing, so
that we get pre-parsed and named function arguments for the CLI.
Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
Diffstat (limited to 'python')
| -rw-r--r-- | python/Makefile.am | 3 | ||||
| -rw-r--r-- | python/clidef.py | 254 | ||||
| -rw-r--r-- | python/clippy/__init__.py | 64 | 
3 files changed, 321 insertions, 0 deletions
diff --git a/python/Makefile.am b/python/Makefile.am new file mode 100644 index 0000000000..4ad1e36b59 --- /dev/null +++ b/python/Makefile.am @@ -0,0 +1,3 @@ +EXTRA_DIST = \ +	clidef.py \ +	clippy/__init__.py diff --git a/python/clidef.py b/python/clidef.py new file mode 100644 index 0000000000..de3a764a0b --- /dev/null +++ b/python/clidef.py @@ -0,0 +1,254 @@ +# 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 + +class StringHandler(RenderHandler): +    argtype = 'const char *' +    decl = Template('const char *$varname = NULL;') +    code = Template('$varname = argv[_i]->arg;') +    drop_str = 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 type(other) in [Prefix4Handler, Prefix6Handler, PrefixGenHandler]: +            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 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 = IN6ADDR_ANY_INIT;') +    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; +}''') + +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, +} + +# core template invoked for each occurence of DEFPY. +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) +{ +	int _i; +	unsigned _fail = 0, _failcnt = 0; +$argdecls +	for (_i = 0; _i < argc; _i++) { +		if (!argv[_i]->varname) +			continue; +		_fail = 0;$argblocks +		if (_fail) +			vty_out (vty, "%% invalid input for %s: %s%s", +				argv[_i]->varname, argv[_i]->arg, VTY_NEWLINE); +		_failcnt += _fail; +	} +	if (_failcnt) +		return CMD_WARNING; +	return ${fnname}_magic(self, vty, argc, argv$arglist); +} + +''') + +# invoked for each named parameter +argblock = Template(''' +		if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock +			$code +		}''') + +def process_file(fn, ofd, dumpfd, all_defun): +    filedata = clippy.parse(fn) + +    for entry in filedata['data']: +        if entry['type'] == 'DEFPY' or (all_defun and entry['type'].startswith('DEFUN')): +            cmddef = entry['args'][2] +            for i in cmddef: +                assert i.startswith('"') and i.endswith('"') +            cmddef = ''.join([i[1:-1] for i in cmddef]) + +            graph = clippy.Graph(cmddef) +            args = OrderedDict() +            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)) + +            #print('-' * 76) +            #pprint(entry) +            #clippy.dump(graph) +            #pprint(args) + +            params = { 'cmddef': cmddef, 'fnname': entry['args'][0][0] } +            argdefs = [] +            argdecls = [] +            arglist = [] +            argblocks = [] +            doc = [] + +            def do_add(handler, 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 attr == '': +                    at = handler.argtype +                    if not at.startswith('const '): +                        at = '. . . ' + at +                    doc.append('\t%-26s %s' % (at, varname)) + +            for varname in args.keys(): +                handler = mix_handlers(args[varname]) +                #print(varname, handler) +                if handler is None: continue +                do_add(handler, varname) +                code = handler.code.substitute({'varname': varname}).replace('\n', '\n\t\t\t') +                strblock = '' +                if not handler.drop_str: +                    do_add(StringHandler(None), '%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) +            ofd.write(templ.substitute(params)) + +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 + +    process_file(args.cfile, ofd, dumpfd, args.all_defun) + +    if args.o is not None: +        clippy.wrdiff(args.o, ofd) diff --git a/python/clippy/__init__.py b/python/clippy/__init__.py new file mode 100644 index 0000000000..82aa9495d4 --- /dev/null +++ b/python/clippy/__init__.py @@ -0,0 +1,64 @@ +# FRR CLI preprocessor +# +# 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 +from _clippy import parse, Graph, GraphNode + +def graph_iterate(graph): +    '''iterator yielding all nodes of a graph + +    nodes arrive in input/definition order, graph circles are avoided. +    ''' + +    queue = [(graph.first(), frozenset(), 0)] +    while len(queue) > 0: +        node, stop, depth = queue.pop(0) +        yield node, depth + +        join = node.join() +        if join is not None: +            queue.insert(0, (join, stop.union(frozenset([node])), depth)) +            join = frozenset([join]) + +        stop = join or stop +        nnext = node.next() +        for n in reversed(nnext): +            if n not in stop and n is not node: +                queue.insert(0, (n, stop, depth + 1)) + +def dump(graph): +    '''print out clippy.Graph''' + +    for i, depth in graph_iterate(graph): +        print('\t%s%s %r' % ('  ' * (depth * 2), i.type, i.text)) + +def wrdiff(filename, buf): +    '''write buffer to file if contents changed''' + +    expl = '' +    if hasattr(buf, 'getvalue'): +        buf = buf.getvalue() +    old = None +    try:    old = open(filename, 'r').read() +    except: pass +    if old == buf: +        # sys.stderr.write('%s unchanged, not written\n' % (filename)) +        return +    with open('.new.' + filename, 'w') as out: +        out.write(buf) +    os.rename('.new.' + filename, filename)  | 
