]>
Commit | Line | Data |
---|---|---|
5578a14d DL |
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 | ||
19 | import clippy, traceback, sys, os | |
20 | from collections import OrderedDict | |
21 | from functools import reduce | |
22 | from pprint import pprint | |
23 | from string import Template | |
24 | from 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 | ||
29 | class 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 | |
0ee0892b | 39 | canfail = True |
4381a59b | 40 | canassert = False |
5578a14d DL |
41 | |
42 | class StringHandler(RenderHandler): | |
43 | argtype = 'const char *' | |
44 | decl = Template('const char *$varname = NULL;') | |
5254bb15 | 45 | code = Template('$varname = (argv[_i]->type == WORD_TKN) ? argv[_i]->text : argv[_i]->arg;') |
5578a14d | 46 | drop_str = True |
0ee0892b | 47 | canfail = False |
4381a59b | 48 | canassert = True |
5578a14d DL |
49 | |
50 | class LongHandler(RenderHandler): | |
51 | argtype = 'long' | |
52 | decl = Template('long $varname = 0;') | |
53 | code = Template('''\ | |
54 | char *_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 | ||
62 | class PrefixBase(RenderHandler): | |
63 | def combine(self, other): | |
64 | if type(self) == type(other): | |
65 | return other | |
473196f6 | 66 | if isinstance(other, PrefixBase): |
5578a14d DL |
67 | return PrefixGenHandler(None) |
68 | return StringHandler(None) | |
69 | deref = '&' | |
70 | class Prefix4Handler(PrefixBase): | |
71 | argtype = 'const struct prefix_ipv4 *' | |
3e300703 | 72 | decl = Template('struct prefix_ipv4 $varname = { };') |
5578a14d DL |
73 | code = Template('_fail = !str2prefix_ipv4(argv[_i]->arg, &$varname);') |
74 | class Prefix6Handler(PrefixBase): | |
75 | argtype = 'const struct prefix_ipv6 *' | |
3e300703 | 76 | decl = Template('struct prefix_ipv6 $varname = { };') |
5578a14d | 77 | code = Template('_fail = !str2prefix_ipv6(argv[_i]->arg, &$varname);') |
473196f6 QY |
78 | class PrefixEthHandler(PrefixBase): |
79 | argtype = 'struct prefix_eth *' | |
3e300703 | 80 | decl = Template('struct prefix_eth $varname = { };') |
473196f6 | 81 | code = Template('_fail = !str2prefix_eth(argv[_i]->arg, &$varname);') |
5578a14d DL |
82 | class PrefixGenHandler(PrefixBase): |
83 | argtype = 'const struct prefix *' | |
3e300703 | 84 | decl = Template('struct prefix $varname = { };') |
5578a14d DL |
85 | code = Template('_fail = !str2prefix(argv[_i]->arg, &$varname);') |
86 | ||
87 | # same for IP addresses. result is union sockunion. | |
88 | class 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) | |
95 | class 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);') | |
99 | class IP6Handler(IPBase): | |
100 | argtype = 'struct in6_addr' | |
3e300703 | 101 | decl = Template('struct in6_addr $varname = {};') |
5578a14d DL |
102 | code = Template('_fail = !inet_pton(AF_INET6, argv[_i]->arg, &$varname);') |
103 | class IPGenHandler(IPBase): | |
104 | argtype = 'const union sockunion *' | |
105 | decl = Template('''union sockunion s__$varname = { .sa.sa_family = AF_UNSPEC }, *$varname = NULL;''') | |
106 | code = Template('''\ | |
107 | if (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 | }''') | |
4381a59b | 116 | canassert = True |
5578a14d DL |
117 | |
118 | def 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 | ||
125 | handlers = { | |
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, | |
473196f6 QY |
133 | 'MAC_TKN': PrefixEthHandler, |
134 | 'MAC_PREFIX_TKN': PrefixEthHandler, | |
5578a14d DL |
135 | } |
136 | ||
137 | # core template invoked for each occurence of DEFPY. | |
0ee0892b DL |
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) | |
5578a14d DL |
142 | templ = Template('''/* $fnname => "$cmddef" */ |
143 | DEFUN_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) | |
149 | funcdecl_$fnname; | |
150 | DEFUN_CMD_FUNC_TEXT($fnname) | |
151 | { | |
0ee0892b | 152 | #if $nonempty /* anything to parse? */ |
5578a14d | 153 | int _i; |
0ee0892b | 154 | #if $canfail /* anything that can fail? */ |
5578a14d | 155 | unsigned _fail = 0, _failcnt = 0; |
0ee0892b | 156 | #endif |
5578a14d DL |
157 | $argdecls |
158 | for (_i = 0; _i < argc; _i++) { | |
159 | if (!argv[_i]->varname) | |
160 | continue; | |
0ee0892b DL |
161 | #if $canfail /* anything that can fail? */ |
162 | _fail = 0; | |
163 | #endif | |
164 | $argblocks | |
165 | #if $canfail /* anything that can fail? */ | |
5578a14d | 166 | if (_fail) |
5c7571d4 | 167 | vty_out (vty, "%% invalid input for %s: %s\\n", |
8e25c8ce | 168 | argv[_i]->varname, argv[_i]->arg); |
5578a14d | 169 | _failcnt += _fail; |
0ee0892b | 170 | #endif |
5578a14d | 171 | } |
0ee0892b | 172 | #if $canfail /* anything that can fail? */ |
5578a14d DL |
173 | if (_failcnt) |
174 | return CMD_WARNING; | |
0ee0892b DL |
175 | #endif |
176 | #endif | |
4381a59b | 177 | $argassert |
5578a14d DL |
178 | return ${fnname}_magic(self, vty, argc, argv$arglist); |
179 | } | |
180 | ||
181 | ''') | |
182 | ||
183 | # invoked for each named parameter | |
184 | argblock = Template(''' | |
185 | if (!strcmp(argv[_i]->varname, \"$varname\")) {$strblock | |
186 | $code | |
187 | }''') | |
188 | ||
4381a59b DL |
189 | def 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 | ||
b41b3f7b DL |
204 | class 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 | ||
224 | def process_file(fn, ofd, dumpfd, all_defun, macros): | |
3779776a | 225 | errors = 0 |
5578a14d DL |
226 | filedata = clippy.parse(fn) |
227 | ||
228 | for entry in filedata['data']: | |
e31f4dbe | 229 | if entry['type'].startswith('DEFPY') or (all_defun and entry['type'].startswith('DEFUN')): |
3779776a DL |
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 | ||
5578a14d | 235 | cmddef = entry['args'][2] |
b41b3f7b | 236 | cmddefx = [] |
3779776a | 237 | for i in cmddef: |
b41b3f7b DL |
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: | |
3779776a | 249 | continue |
b41b3f7b | 250 | cmddef = ''.join([i for i in cmddefx]) |
5578a14d DL |
251 | |
252 | graph = clippy.Graph(cmddef) | |
253 | args = OrderedDict() | |
4381a59b | 254 | always_args = set() |
5578a14d DL |
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)) | |
4381a59b DL |
262 | always_args.add(token.varname) |
263 | ||
264 | get_always_args(graph.first(), always_args) | |
5578a14d DL |
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 = [] | |
4381a59b | 276 | argassert = [] |
5578a14d | 277 | doc = [] |
0ee0892b | 278 | canfail = 0 |
5578a14d | 279 | |
4381a59b | 280 | def do_add(handler, basename, varname, attr = ''): |
5578a14d DL |
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)) | |
4381a59b DL |
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)) | |
5578a14d DL |
289 | if attr == '': |
290 | at = handler.argtype | |
291 | if not at.startswith('const '): | |
292 | at = '. . . ' + at | |
4381a59b | 293 | doc.append('\t%-26s %s %s' % (at, 'alw' if basename in always_args else 'opt', varname)) |
5578a14d DL |
294 | |
295 | for varname in args.keys(): | |
296 | handler = mix_handlers(args[varname]) | |
297 | #print(varname, handler) | |
298 | if handler is None: continue | |
4381a59b | 299 | do_add(handler, varname, varname) |
5578a14d | 300 | code = handler.code.substitute({'varname': varname}).replace('\n', '\n\t\t\t') |
0ee0892b DL |
301 | if handler.canfail: |
302 | canfail = 1 | |
5578a14d DL |
303 | strblock = '' |
304 | if not handler.drop_str: | |
4381a59b | 305 | do_add(StringHandler(None), varname, '%s_str' % (varname), ' __attribute__ ((unused))') |
5578a14d DL |
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) | |
0ee0892b DL |
319 | params['canfail'] = canfail |
320 | params['nonempty'] = len(argblocks) | |
4381a59b | 321 | params['argassert'] = ''.join(argassert) |
5578a14d DL |
322 | ofd.write(templ.substitute(params)) |
323 | ||
3779776a DL |
324 | return errors |
325 | ||
5578a14d DL |
326 | if __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 | ||
c4ca3ef5 DL |
349 | basepath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
350 | ||
b41b3f7b DL |
351 | macros = Macros() |
352 | macros.load('lib/route_types.h') | |
c4ca3ef5 | 353 | macros.load(os.path.join(basepath, 'lib/command.h')) |
ed18356f | 354 | macros.load(os.path.join(basepath, 'bgpd/bgp_vty.h')) |
b41b3f7b DL |
355 | # sigh :( |
356 | macros['PROTO_REDIST_STR'] = 'FRR_REDIST_STR_ISISD' | |
357 | ||
358 | errors = process_file(args.cfile, ofd, dumpfd, args.all_defun, macros) | |
3779776a DL |
359 | if errors != 0: |
360 | sys.exit(1) | |
5578a14d DL |
361 | |
362 | if args.o is not None: | |
b716e79a | 363 | clippy.wrdiff(args.o, ofd, [args.cfile, os.path.realpath(__file__), sys.executable]) |