misc: vpe.api messages dynamically allocated
[vpp.git] / src / tools / vppapitrace / vppapitrace.py
1 #!/usr/bin/env python3
2
3 #
4 # Copyright (c) 2019 Cisco and/or its affiliates.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at:
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 #
19 # Convert from VPP API trace to JSON.
20
21 import argparse
22 import struct
23 import sys
24 import logging
25 import json
26 from ipaddress import *
27 from collections import namedtuple
28 from vpp_papi import MACAddress, VPPApiJSONFiles
29 import base64
30 import os
31 import textwrap
32
33 def serialize_likely_small_unsigned_integer(x):
34     r = x
35
36     # Low bit set means it fits into 1 byte.
37     if r < (1 << 7):
38         return struct.pack("B", 1 + 2 * r)
39
40     # Low 2 bits 1 0 means it fits into 2 bytes.
41     r -= (1 << 7)
42     if r < (1 << 14):
43         return struct.pack("<H", 4 * r + 2)
44
45     r -= (1 << 14)
46     if r < (1 << 29):
47         return struct.pack("<I", 8 * r + 4)
48
49     return struct.pack("<BQ", 0, x)
50
51
52 def unserialize_likely_small_unsigned_integer(data, offset):
53     y = struct.unpack_from("B", data, offset)[0]
54     if y & 1:
55         return y // 2, 1
56     r = 1 << 7
57     if y & 2:
58         p = struct.unpack_from("B", data, offset + 1)[0]
59         r += (y // 4) + (p << 6)
60         return r, 2
61     r += 1 << 14
62     if y & 4:
63         (p1, p2, p3) = struct.unpack_from("BBB", data, offset+1)
64         r += ((y // 8) + (p1 << (5 + 8 * 0))
65               + (p2 << (5 + 8 * 1)) + (p3 << (5 + 8 * 2)))
66         return r, 3
67     return struct.unpack_from(">Q", data, offset+1)[0], 8
68
69
70 def serialize_cstring(s):
71     bstring = s.encode('utf8')
72     l = len(bstring)
73     b = serialize_likely_small_unsigned_integer(l)
74     b += struct.pack('{}s'.format(l), bstring)
75     return b
76
77
78 def unserialize_cstring(data, offset):
79     l, size = unserialize_likely_small_unsigned_integer(data, offset)
80     name = struct.unpack_from('{}s'.format(l), data, offset+size)[0]
81     return name.decode('utf8'), size + len(name)
82
83
84 def unserialize_msgtbl(data, offset):
85     msgtable_by_id = {}
86     msgtable_by_name = {}
87     i = 0
88     nmsg = struct.unpack_from(">I", data, offset)[0]
89     o = 4
90     while i < nmsg:
91         (msgid, size) = unserialize_likely_small_unsigned_integer(
92             data, offset + o)
93         o += size
94         (name, size) = unserialize_cstring(data, offset + o)
95         o += size
96         msgtable_by_id[msgid] = name
97         msgtable_by_name[name] = msgid
98
99         i += 1
100     return msgtable_by_id, msgtable_by_name, o
101
102
103 def serialize_msgtbl(messages):
104     offset = 0
105     # XXX 100K?
106     data = bytearray(100000)
107     nmsg = len(messages)
108     data = struct.pack(">I", nmsg)
109
110     for k, v in messages.items():
111         name = k + '_' + v.crc[2:]
112         data += serialize_likely_small_unsigned_integer(v._vl_msg_id)
113         data += serialize_cstring(name)
114     return data
115
116
117 def apitrace2json(messages, filename):
118     result = []
119     with open(filename, 'rb') as file:
120         bytes_read = file.read()
121         # Read header
122         (nitems, msgtbl_size, wrapped) = struct.unpack_from(">IIB",
123                                                             bytes_read, 0)
124         logging.debug('nitems: {} message table size: {} wrapped: {}'
125                       .format(nitems, msgtbl_size, wrapped))
126         if wrapped:
127             sys.stdout.write('Wrapped/incomplete trace, results may vary')
128         offset = 9
129
130         msgtbl_by_id, msgtbl_by_name, size = unserialize_msgtbl(bytes_read,
131                                                                 offset)
132         offset += size
133
134         i = 0
135         while i < nitems:
136             size = struct.unpack_from(">I", bytes_read, offset)[0]
137             offset += 4
138             if size == 0:
139                 break
140             msgid = struct.unpack_from(">H", bytes_read, offset)[0]
141             name = msgtbl_by_id[msgid]
142             n = name[:name.rfind("_")]
143             msgobj = messages[n]
144             if n + '_' + msgobj.crc[2:] != name:
145                 sys.exit("CRC Mismatch between JSON API definition "
146                          "and trace. {}".format(name))
147
148             x, s = msgobj.unpack(bytes_read[offset:offset+size])
149             msgname = type(x).__name__
150             offset += size
151             # Replace named tuple illegal _0
152             y = x._asdict()
153             y.pop('_0')
154             result.append({'name': msgname, 'args': y})
155             i += 1
156
157     file.close()
158     return result
159
160
161 def json2apitrace(messages, filename):
162     """Input JSON file and API message definition. Output API trace
163     bytestring."""
164
165     msgs = []
166     with open(filename, 'r') as file:
167         msgs = json.load(file, object_hook=vpp_decode)
168     result = b''
169     for m in msgs:
170         name = m['name']
171         msgobj = messages[name]
172         m['args']['_vl_msg_id'] = messages[name]._vl_msg_id
173         b = msgobj.pack(m['args'])
174
175         result += struct.pack('>I', len(b))
176         result += b
177     return len(msgs), result
178
179
180 class VPPEncoder(json.JSONEncoder):
181     def default(self, o):
182         if type(o) is bytes:
183             return "base64:" + base64.b64encode(o).decode('utf-8')
184         # Let the base class default method raise the TypeError
185         return json.JSONEncoder.default(self, o)
186
187     def encode(self, obj):
188         def hint_tuples(item):
189             if isinstance(item, tuple):
190                 return hint_tuples(item._asdict())
191             if isinstance(item, list):
192                 return [hint_tuples(e) for e in item]
193             if isinstance(item, dict):
194                 return {key: hint_tuples(value) for key, value in item.items()}
195             else:
196                 return item
197
198         return super(VPPEncoder, self).encode(hint_tuples(obj))
199
200
201 def vpp_decode(obj):
202     for k, v in obj.items():
203         if type(v) is str and v.startswith('base64:'):
204             s = v.lstrip('base64:')
205             obj[k] = base64.b64decode(v[7:])
206     return obj
207
208
209 def vpp_encoder(obj):
210     if isinstance(obj, IPv6Network):
211         return str(obj)
212     if isinstance(obj, IPv4Network):
213         return str(obj)
214     if isinstance(obj, IPv6Address):
215         return str(obj)
216     if isinstance(obj, IPv4Address):
217         return str(obj)
218     if isinstance(obj, MACAddress):
219         return str(obj)
220     if type(obj) is bytes:
221         return "base64:" + base64.b64encode(obj).decode('ascii')
222     raise TypeError('Unknown object {} {}\n'.format(type(obj), obj))
223
224 message_filter = {
225     'control_ping',
226     'memclnt_create',
227     'memclnt_delete',
228     'get_first_msg_id',
229 }
230
231 argument_filter = {
232     'client_index',
233     'context',
234 }
235
236 def topython(messages, services):
237     import pprint
238     pp = pprint.PrettyPrinter()
239
240     s = '''\
241 #!/usr/bin/env python3
242 from vpp_papi import VPP, VppEnum
243 vpp = VPP(use_socket=True)
244 vpp.connect(name='vppapitrace')
245 '''
246
247     for m in messages:
248         if m['name'] not in services:
249             s += '# ignoring reply message: {}\n'.format(m['name'])
250             continue
251         if m['name'] in message_filter:
252             s += '# ignoring message {}\n'.format(m['name'])
253             continue
254         for k in argument_filter:
255             try:
256                 m['args'].pop(k)
257             except KeyError:
258                 pass
259         a = pp.pformat(m['args'])
260         s += 'rv = vpp.api.{}(**{})\n'.format(m['name'], a)
261         s += 'print("RV:", rv)\n'
262     s += 'vpp.disconnect()\n'
263
264     return s
265
266 def todump_items(k, v, level):
267     klen = len(k) if k else 0
268     spaces = '  ' * level + ' ' * (klen + 3)
269     wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent=spaces, width=60)
270     s = ''
271     if type(v) is dict:
272         if k:
273             s += '   ' * level + '{}:\n'.format(k)
274         for k2, v2 in v.items():
275             s += todump_items(k2, v2, level + 1)
276         return s
277
278     if type(v) is list:
279         for v2 in v:
280             s += '{}'.format(todump_items(k, v2, level))
281         return s
282
283     if type(v) is bytes:
284         w = wrapper.fill(bytes.hex(v))
285         s += '   ' * level + '{}: {}\n'.format(k, w)
286     else:
287         if type(v) is str:
288             v = wrapper.fill(v)
289         s += '   ' * level + '{}: {}\n'.format(k, v)
290     return s
291
292
293 def todump(messages, services):
294     import pprint
295     pp = pprint.PrettyPrinter()
296
297     s = ''
298     for m in messages:
299         if m['name'] not in services:
300             s += '# ignoring reply message: {}\n'.format(m['name'])
301             continue
302         #if m['name'] in message_filter:
303         #    s += '# ignoring message {}\n'.format(m['name'])
304         #    continue
305         for k in argument_filter:
306             try:
307                 m['args'].pop(k)
308             except KeyError:
309                 pass
310         a = pp.pformat(m['args'])
311         s += '{}:\n'.format(m['name'])
312         s += todump_items(None, m['args'], 0)
313     return s
314
315
316 def init_api(apidir):
317     # Read API definitions
318     apifiles = VPPApiJSONFiles.find_api_files(api_dir=apidir)
319     messages = {}
320     services = {}
321     for file in apifiles:
322         with open(file) as apidef_file:
323             m, s = VPPApiJSONFiles.process_json_file(apidef_file)
324             messages.update(m)
325             services.update(s)
326     return messages, services
327
328
329 def replaymsgs(vpp, msgs):
330     for m in msgs:
331         name = m['name']
332         if name not in vpp.services:
333             continue
334         if name == 'control_ping':
335             continue
336         try:
337             m['args'].pop('client_index')
338         except KeyError:
339             pass
340         if m['args']['context'] == 0:
341             m['args']['context'] = 1
342         f = vpp.get_function(name)
343         rv = f(**m['args'])
344         print('RV {}'.format(rv))
345
346
347 def replay(args):
348     """Replay into running VPP instance"""
349
350     from vpp_papi import VPP
351
352     JSON = 1
353     APITRACE = 2
354
355     filename, file_extension = os.path.splitext(args.input)
356     input_type = JSON if file_extension == '.json' else APITRACE
357
358     vpp = VPP(use_socket=args.socket)
359     rv = vpp.connect(name='vppapireplay', chroot_prefix=args.shmprefix)
360     if rv != 0:
361         sys.exit('Cannot connect to VPP')
362
363     if input_type == JSON:
364         with open(args.input, 'r') as file:
365             msgs = json.load(file, object_hook=vpp_decode)
366     else:
367         msgs = apitrace2json(messages, args.input)
368
369     replaymsgs(vpp, msgs)
370
371     vpp.disconnect()
372
373
374 def generate(args):
375     """Generate JSON"""
376
377     JSON = 1
378     APITRACE = 2
379     PYTHON = 3
380     DUMP = 4
381
382     filename, file_extension = os.path.splitext(args.input)
383     input_type = JSON if file_extension == '.json' else APITRACE
384     filename, file_extension = os.path.splitext(args.output)
385
386     if args.todump:
387         output_type = DUMP
388     else:
389         if file_extension == '.json' or filename == '-':
390             output_type = JSON
391         elif file_extension == '.py':
392             output_type = PYTHON
393         else:
394             output_type = APITRACE
395
396     if input_type == output_type:
397         sys.exit("error: Nothing to convert between")
398
399     if input_type != JSON and output_type == APITRACE:
400         sys.exit("error: Input file must be JSON file: {}".format(args.input))
401
402     messages, services = init_api(args.apidir)
403
404     if input_type == JSON and output_type == APITRACE:
405         i = 0
406         for k, v in messages.items():
407             v._vl_msg_id = i
408             i += 1
409
410         n, result = json2apitrace(messages, args.input)
411         msgtbl = serialize_msgtbl(messages)
412
413         print('API messages: {}'.format(n))
414         header = struct.pack(">IIB", n, len(msgtbl), 0)
415
416         with open(args.output, 'wb') as outfile:
417             outfile.write(header)
418             outfile.write(msgtbl)
419             outfile.write(result)
420
421         return
422
423     if input_type == APITRACE:
424         result = apitrace2json(messages, args.input)
425         if output_type == PYTHON:
426             s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
427             x = json.loads(s, object_hook=vpp_decode)
428             s = topython(x, services)
429         elif output_type == DUMP:
430             s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
431             x = json.loads(s, object_hook=vpp_decode)
432             s = todump(x, services)
433         else:
434             s = json.dumps(result, cls=VPPEncoder,
435                            default=vpp_encoder, indent=4 * ' ')
436     elif output_type == PYTHON:
437         with open(args.input, 'r') as file:
438             x = json.load(file, object_hook=vpp_decode)
439             s = topython(x, services)
440     else:
441         sys.exit('Input file must be API trace file: {}'.format(args.input))
442
443     if args.output == '-':
444         sys.stdout.write(s + '\n')
445     else:
446         print('Generating {} from API trace: {}'
447               .format(args.output, args.input))
448         with open(args.output, 'w') as outfile:
449             outfile.write(s)
450
451 def general(args):
452     return
453
454 def main():
455     parser = argparse.ArgumentParser()
456     parser.add_argument('--debug', action='store_true',
457                         help='enable debug mode')
458     parser.add_argument('--apidir',
459                         help='Location of JSON API definitions')
460
461     parser.set_defaults(func=general)
462     subparsers = parser.add_subparsers(title='subcommands',
463                                        description='valid subcommands',
464                                        help='additional help')
465
466     parser_convert = subparsers.add_parser('convert',
467                                            help='Convert API trace to JSON or Python and back')
468     parser_convert.add_argument('input',
469                                 help='Input file (API trace | JSON)')
470     parser_convert.add_argument('--todump', action='store_true', help='Output text format')
471     parser_convert.add_argument('output',
472                                 help='Output file (Python | JSON | API trace)')
473     parser_convert.set_defaults(func=generate)
474
475
476     parser_replay = subparsers.add_parser('replay',
477                                           help='Replay messages to running VPP instance')
478     parser_replay.add_argument('input', help='Input file (API trace | JSON)')
479     parser_replay.add_argument('--socket', action='store_true',
480                                help='use default socket to connect to VPP')
481     parser_replay.add_argument('--shmprefix',
482                                help='connect to VPP on shared memory prefix')
483     parser_replay.set_defaults(func=replay)
484
485     args = parser.parse_args()
486     if args.debug:
487         logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
488
489     args.func(args)
490
491
492 main()