]> git.puffer.fish Git - matthieu/frr.git/commitdiff
python: clidef.py
authorDavid Lamparter <equinox@opensourcerouting.org>
Thu, 11 May 2017 13:36:04 +0000 (15:36 +0200)
committerDavid Lamparter <equinox@opensourcerouting.org>
Wed, 14 Jun 2017 17:29:26 +0000 (19:29 +0200)
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>
.gitignore
Makefile.am
common.am
configure.ac
lib/command.h
python/Makefile.am [new file with mode: 0644]
python/clidef.py [new file with mode: 0644]
python/clippy/__init__.py [new file with mode: 0644]

index 5b88cf25afcf8ac71b2df1586ba9bbaf93c5f276..62aa48e22f77efc82b562aab7c530b1373a2b0f1 100644 (file)
@@ -61,6 +61,7 @@ debian/quagga.prerm.debhelper
 debian/quagga.substvars
 debian/quagga/
 debian/tmp/
+*.pyc
 *.swp
 cscope.*
 *.pb.h
index 6d787e3c6e499a28c7bb11db0e5347b1c84b5ce6..d18837c083f7bd95fd43d13fc7d450554316acee 100644 (file)
@@ -9,7 +9,9 @@ SUBDIRS = lib qpb fpm @ZEBRA@ @LIBRFP@ @RFPTEST@ \
 DIST_SUBDIRS = lib qpb fpm zebra bgpd ripd ripngd ospfd ospf6d ldpd \
          isisd watchfrr vtysh ospfclient doc m4 pkgsrc redhat tests \
          solaris pimd nhrpd eigrpd @LIBRFP@ @RFPTEST@ tools snapcraft \
-         babeld
+         babeld \
+         python \
+         # end
 
 EXTRA_DIST = aclocal.m4 SERVICES REPORTING-BUGS \
        update-autotools \
index d01d673ae644aaf2d05b62169af334e4f38b0631..0ccc4c9fd153984c578b0788de20c217f5df11e0 100644 (file)
--- a/common.am
+++ b/common.am
@@ -3,6 +3,17 @@
 # tree.
 #
 
+AM_V_CLIPPY = $(am__v_CLIPPY_$(V))
+am__v_CLIPPY_ = $(am__v_CLIPPY_$(AM_DEFAULT_VERBOSITY))
+am__v_CLIPPY_0 = @echo "  CLIPPY  " $@;
+am__v_CLIPPY_1 =
+
+SUFFIXES = _clippy.c
+.c_clippy.c:
+       $(AM_V_at)$(MAKE) -C $(top_builddir)/$(CLIPPYDIR) clippy
+       $(AM_V_CLIPPY)$(top_builddir)/$(CLIPPYDIR)/clippy $(top_srcdir)/python/clidef.py $< > $@.tmp
+       @{ test -f $@ && diff $@.tmp $@ >/dev/null 2>/dev/null; } && rm $@.tmp || mv $@.tmp $@
+
 if HAVE_PROTOBUF
 
 # Uncomment to use an non-system version of libprotobuf-c.
index ca8eae9057a25de6fa09eb33a3234af273a71c69..1dd570fcee09505a40902731b1ca44f2d94179c5 100755 (executable)
@@ -1983,6 +1983,7 @@ AC_CONFIG_FILES([Makefile lib/Makefile qpb/Makefile zebra/Makefile ripd/Makefile
          redhat/Makefile
          tools/Makefile
          pkgsrc/Makefile
+         python/Makefile
          fpm/Makefile
          redhat/frr.spec 
          snapcraft/Makefile
index ada9003cc96a7430977eb12aacaa3d0f7732075b..927c04006c5d6bb36029046e22d90c4e4fb88f75 100644 (file)
@@ -207,6 +207,10 @@ struct cmd_node
      int argc __attribute__ ((unused)), \
      struct cmd_token *argv[] __attribute__ ((unused)) )
 
+#define DEFPY(funcname, cmdname, cmdstr, helpstr) \
+  DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, 0, 0) \
+  funcdecl_##funcname
+
 #define DEFUN(funcname, cmdname, cmdstr, helpstr) \
   DEFUN_CMD_FUNC_DECL(funcname) \
   DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, 0, 0) \
@@ -274,6 +278,9 @@ struct cmd_node
 #define ALIAS_SH_DEPRECATED(daemon, funcname, cmdname, cmdstr, helpstr) \
   DEFUN_CMD_ELEMENT(funcname, cmdname, cmdstr, helpstr, CMD_ATTR_DEPRECATED, daemon)
 
+#else /* VTYSH_EXTRACT_PL */
+#define DEFPY(funcname, cmdname, cmdstr, helpstr) \
+  DEFUN(funcname, cmdname, cmdstr, helpstr)
 #endif /* VTYSH_EXTRACT_PL */
 
 /* Some macroes */
diff --git a/python/Makefile.am b/python/Makefile.am
new file mode 100644 (file)
index 0000000..4ad1e36
--- /dev/null
@@ -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 (file)
index 0000000..de3a764
--- /dev/null
@@ -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 (file)
index 0000000..82aa949
--- /dev/null
@@ -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)