1# FRR CLI preprocessor (DEFPY) 2# 3# Copyright (C) 2017 David Lamparter for NetDEF, Inc. 4# 5# This program is free software; you can redistribute it and/or modify it 6# under the terms of the GNU General Public License as published by the Free 7# Software Foundation; either version 2 of the License, or (at your option) 8# any later version. 9# 10# This program is distributed in the hope that it will be useful, but WITHOUT 11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 12# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 13# more details. 14# 15# You should have received a copy of the GNU General Public License along 16# with this program; see the file COPYING; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 19import clippy, traceback, sys, os 20from collections import OrderedDict 21from functools import reduce 22from pprint import pprint 23from string import Template 24from io import StringIO 25 26# the various handlers generate output C code for a particular type of 27# CLI token, choosing the most useful output C type. 28 29class RenderHandler(object): 30 def __init__(self, token): 31 pass 32 def combine(self, other): 33 if type(self) == type(other): 34 return other 35 return StringHandler(None) 36 37 deref = '' 38 drop_str = False 39 canfail = True 40 canassert = False 41 42class StringHandler(RenderHandler): 43 argtype = 'const char *' 44 decl = Template('const char *$varname = NULL;') 45 code = Template('$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;') 46 drop_str = True 47 canfail = False 48 canassert = True 49 50class LongHandler(RenderHandler): 51 argtype = 'long' 52 decl = Template('long $varname = 0;') 53 code = Template('''\ 54char *_end; 55$varname = strtol(argv[_i]->arg, &_end, 10); 56_fail = (_end == argv[_i]->arg) || (*_end != '\\0');''') 57 58# A.B.C.D/M (prefix_ipv4) and 59# X:X::X:X/M (prefix_ipv6) are "compatible" and can merge into a 60# struct prefix: 61 62class PrefixBase(RenderHandler): 63 def combine(self, other): 64 if type(self) == type(other): 65 return other 66 if isinstance(other, PrefixBase): 67 return PrefixGenHandler(None) 68 return StringHandler(None) 69 deref = '&' 70class Prefix4Handler(PrefixBase): 71 argtype = 'const struct prefix_ipv4 *' 72 decl = Template('struct prefix_ipv4 $varname = { };') 73 code = Template('_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);') 74class Prefix6Handler(PrefixBase): 75 argtype = 'const struct prefix_ipv6 *' 76 decl = Template('struct prefix_ipv6 $varname = { };') 77 code = Template('_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);') 78class PrefixEthHandler(PrefixBase): 79 argtype = 'struct prefix_eth *' 80 decl = Template('struct prefix_eth $varname = { };') 81 code = Template('_fail = !str2prefix_eth(argv[_i]->arg, &$varname);') 82class PrefixGenHandler(PrefixBase): 83 argtype = 'const struct prefix *' 84 decl = Template('struct prefix $varname = { };') 85 code = Template('_fail = !str2prefix(argv[_i]->arg, &$varname);') 86 87# same for IP addresses. result is union sockunion. 88class IPBase(RenderHandler): 89 def combine(self, other): 90 if type(self) == type(other): 91 return other 92 if type(other) in [IP4Handler, IP6Handler, IPGenHandler]: 93 return IPGenHandler(None) 94 return StringHandler(None) 95class IP4Handler(IPBase): 96 argtype = 'struct in_addr' 97 decl = Template('struct in_addr $varname = { INADDR_ANY };') 98 code = Template('_fail = !inet_aton(argv[_i]->arg, &$varname);') 99class IP6Handler(IPBase): 100 argtype = 'struct in6_addr' 101 decl = Template('struct in6_addr $varname = {};') 102 code = Template('_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);') 103class IPGenHandler(IPBase): 104 argtype = 'const union sockunion *' 105 decl = Template('''union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;''') 106 code = Template('''\ 107if (argv[_i]->text[0] == 'X') { 108 s__$varname.sa.sa_family = AF_INET6; 109 _fail = !inet_pton(AF_INET6, argv[_i]->arg, &s__$varname.sin6.sin6_addr); 110 $varname = &s__$varname; 111} else { 112 s__$varname.sa.sa_family = AF_INET; 113 _fail = !inet_aton(argv[_i]->arg, &s__$varname.sin.sin_addr); 114 $varname = &s__$varname; 115}''') 116 canassert = True 117 118def mix_handlers(handlers): 119 def combine(a, b): 120 if a is None: 121 return b 122 return a.combine(b) 123 return reduce(combine, handlers, None) 124 125handlers = { 126 'WORD_TKN': StringHandler, 127 'VARIABLE_TKN': StringHandler, 128 'RANGE_TKN': LongHandler, 129 'IPV4_TKN': IP4Handler, 130 'IPV4_PREFIX_TKN': Prefix4Handler, 131 'IPV6_TKN': IP6Handler, 132 'IPV6_PREFIX_TKN': Prefix6Handler, 133 'MAC_TKN': PrefixEthHandler, 134 'MAC_PREFIX_TKN': PrefixEthHandler, 135} 136 137# core template invoked for each occurence of DEFPY. 138# 139# the "#if $..." bits are there to keep this template unified into one 140# common form, without requiring a more advanced template engine (e.g. 141# jinja2) 142templ = Template('''/* $fnname => "$cmddef" */ 143DEFUN_CMD_FUNC_DECL($fnname) 144#define funcdecl_$fnname static int ${fnname}_magic(\\ 145 const struct cmd_element *self __attribute__ ((unused)),\\ 146 struct vty *vty __attribute__ ((unused)),\\ 147 int argc __attribute__ ((unused)),\\ 148 struct cmd_token *argv[] __attribute__ ((unused))$argdefs) 149funcdecl_$fnname; 150DEFUN_CMD_FUNC_TEXT($fnname) 151{ 152#if $nonempty /* anything to parse? */ 153 int _i; 154#if $canfail /* anything that can fail? */ 155 unsigned _fail = 0, _failcnt = 0; 156#endif 157$argdecls 158 for (_i = 0; _i < argc; _i++) { 159 if (!argv[_i]->varname) 160 continue; 161#if $canfail /* anything that can fail? */ 162 _fail = 0; 163#endif 164$argblocks 165#if $canfail /* anything that can fail? */ 166 if (_fail) 167 vty_out (vty, "%% invalid input for %s: %s\\n", 168 argv[_i]->varname, argv[_i]->arg); 169 _failcnt += _fail; 170#endif 171 } 172#if $canfail /* anything that can fail? */ 173 if (_failcnt) 174 return CMD_WARNING; 175#endif 176#endif 177$argassert 178 return ${fnname}_magic(self, vty, argc, argv$arglist); 179} 180 181''') 182 183# invoked for each named parameter 184argblock = Template(''' 185 if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock 186 $code 187 }''') 188 189def get_always_args(token, always_args, args = [], stack = []): 190 if token in stack: 191 return 192 if token.type == 'END_TKN': 193 for arg in list(always_args): 194 if arg not in args: 195 always_args.remove(arg) 196 return 197 198 stack = stack + [token] 199 if token.type in handlers and token.varname is not None: 200 args = args + [token.varname] 201 for nexttkn in token.next(): 202 get_always_args(nexttkn, always_args, args, stack) 203 204class Macros(dict): 205 def load(self, filename): 206 filedata = clippy.parse(filename) 207 for entry in filedata['data']: 208 if entry['type'] != 'PREPROC': 209 continue 210 ppdir = entry['line'].lstrip().split(None, 1) 211 if ppdir[0] != 'define' or len(ppdir) != 2: 212 continue 213 ppdef = ppdir[1].split(None, 1) 214 name = ppdef[0] 215 if '(' in name: 216 continue 217 val = ppdef[1] if len(ppdef) == 2 else '' 218 219 val = val.strip(' \t\n\\') 220 if name in self: 221 sys.stderr.write('warning: macro %s redefined!\n' % (name)) 222 self[name] = val 223 224def process_file(fn, ofd, dumpfd, all_defun, macros): 225 errors = 0 226 filedata = clippy.parse(fn) 227 228 for entry in filedata['data']: 229 if entry['type'].startswith('DEFPY') or (all_defun and entry['type'].startswith('DEFUN')): 230 if len(entry['args'][0]) != 1: 231 sys.stderr.write('%s:%d: DEFPY function name not parseable (%r)\n' % (fn, entry['lineno'], entry['args'][0])) 232 errors += 1 233 continue 234 235 cmddef = entry['args'][2] 236 cmddefx = [] 237 for i in cmddef: 238 while i in macros: 239 i = macros[i] 240 if i.startswith('"') and i.endswith('"'): 241 cmddefx.append(i[1:-1]) 242 continue 243 244 sys.stderr.write('%s:%d: DEFPY command string not parseable (%r)\n' % (fn, entry['lineno'], cmddef)) 245 errors += 1 246 cmddefx = None 247 break 248 if cmddefx is None: 249 continue 250 cmddef = ''.join([i for i in cmddefx]) 251 252 graph = clippy.Graph(cmddef) 253 args = OrderedDict() 254 always_args = set() 255 for token, depth in clippy.graph_iterate(graph): 256 if token.type not in handlers: 257 continue 258 if token.varname is None: 259 continue 260 arg = args.setdefault(token.varname, []) 261 arg.append(handlers[token.type](token)) 262 always_args.add(token.varname) 263 264 get_always_args(graph.first(), always_args) 265 266 #print('-' * 76) 267 #pprint(entry) 268 #clippy.dump(graph) 269 #pprint(args) 270 271 params = { 'cmddef': cmddef, 'fnname': entry['args'][0][0] } 272 argdefs = [] 273 argdecls = [] 274 arglist = [] 275 argblocks = [] 276 argassert = [] 277 doc = [] 278 canfail = 0 279 280 def do_add(handler, basename, varname, attr = ''): 281 argdefs.append(',\\\n\t%s %s%s' % (handler.argtype, varname, attr)) 282 argdecls.append('\t%s\n' % (handler.decl.substitute({'varname': varname}).replace('\n', '\n\t'))) 283 arglist.append(', %s%s' % (handler.deref, varname)) 284 if basename in always_args and handler.canassert: 285 argassert.append('''\tif (!%s) { 286\t\tvty_out(vty, "Internal CLI error [%%s]\\n", "%s"); 287\t\treturn CMD_WARNING; 288\t}\n''' % (varname, varname)) 289 if attr == '': 290 at = handler.argtype 291 if not at.startswith('const '): 292 at = '. . . ' + at 293 doc.append('\t%-26s %s %s' % (at, 'alw' if basename in always_args else 'opt', varname)) 294 295 for varname in args.keys(): 296 handler = mix_handlers(args[varname]) 297 #print(varname, handler) 298 if handler is None: continue 299 do_add(handler, varname, varname) 300 code = handler.code.substitute({'varname': varname}).replace('\n', '\n\t\t\t') 301 if handler.canfail: 302 canfail = 1 303 strblock = '' 304 if not handler.drop_str: 305 do_add(StringHandler(None), varname, '%s_str' % (varname), ' __attribute__ ((unused))') 306 strblock = '\n\t\t\t%s_str = argv[_i]->arg;' % (varname) 307 argblocks.append(argblock.substitute({'varname': varname, 'strblock': strblock, 'code': code})) 308 309 if dumpfd is not None: 310 if len(arglist) > 0: 311 dumpfd.write('"%s":\n%s\n\n' % (cmddef, '\n'.join(doc))) 312 else: 313 dumpfd.write('"%s":\n\t---- no magic arguments ----\n\n' % (cmddef)) 314 315 params['argdefs'] = ''.join(argdefs) 316 params['argdecls'] = ''.join(argdecls) 317 params['arglist'] = ''.join(arglist) 318 params['argblocks'] = ''.join(argblocks) 319 params['canfail'] = canfail 320 params['nonempty'] = len(argblocks) 321 params['argassert'] = ''.join(argassert) 322 ofd.write(templ.substitute(params)) 323 324 return errors 325 326if __name__ == '__main__': 327 import argparse 328 329 argp = argparse.ArgumentParser(description = 'FRR CLI preprocessor in Python') 330 argp.add_argument('--all-defun', action = 'store_const', const = True, 331 help = 'process DEFUN() statements in addition to DEFPY()') 332 argp.add_argument('--show', action = 'store_const', const = True, 333 help = 'print out list of arguments and types for each definition') 334 argp.add_argument('-o', type = str, metavar = 'OUTFILE', 335 help = 'output C file name') 336 argp.add_argument('cfile', type = str) 337 args = argp.parse_args() 338 339 dumpfd = None 340 if args.o is not None: 341 ofd = StringIO() 342 if args.show: 343 dumpfd = sys.stdout 344 else: 345 ofd = sys.stdout 346 if args.show: 347 dumpfd = sys.stderr 348 349 basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 350 351 macros = Macros() 352 macros.load('lib/route_types.h') 353 macros.load(os.path.join(basepath, 'lib/command.h')) 354 macros.load(os.path.join(basepath, 'bgpd/bgp_vty.h')) 355 # sigh :( 356 macros['PROTO_REDIST_STR'] = 'FRR_REDIST_STR_ISISD' 357 358 errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros) 359 if errors != 0: 360 sys.exit(1) 361 362 if args.o is not None: 363 clippy.wrdiff(args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable]) 364