vppapitrace: add text output
[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     data = bytearray(100000)
106     nmsg = len(messages)
107     data = struct.pack(">I", nmsg)
108
109     for k, v in messages.items():
110         name = k + '_' + v.crc[2:]
111         data += serialize_likely_small_unsigned_integer(v._vl_msg_id)
112         data += serialize_cstring(name)
113     return data
114
115
116 def apitrace2json(messages, filename):
117     result = []
118     with open(filename, 'rb') as file:
119         bytes_read = file.read()
120         # Read header
121         (nitems, msgtbl_size, wrapped) = struct.unpack_from(">IIB",
122                                                             bytes_read, 0)
123         logging.debug('nitems: {} message table size: {} wrapped: {}'
124                       .format(nitems, msgtbl_size, wrapped))
125         if wrapped:
126             sys.stdout.write('Wrapped/incomplete trace, results may vary')
127         offset = 9
128
129         msgtbl_by_id, msgtbl_by_name, size = unserialize_msgtbl(bytes_read,
130                                                                 offset)
131         offset += size
132
133         i = 0
134         while i < nitems:
135             size = struct.unpack_from(">I", bytes_read, offset)[0]
136             offset += 4
137             if size == 0:
138                 break
139             msgid = struct.unpack_from(">H", bytes_read, offset)[0]
140             name = msgtbl_by_id[msgid]
141             n = name[:name.rfind("_")]
142             msgobj = messages[n]
143             if n + '_' + msgobj.crc[2:] != name:
144                 sys.exit("CRC Mismatch between JSON API definition "
145                          "and trace. {}".format(name))
146
147             x, s = msgobj.unpack(bytes_read[offset:offset+size])
148             msgname = type(x).__name__
149             offset += size
150             # Replace named tuple illegal _0
151             y = x._asdict()
152             y.pop('_0')
153             result.append({'name': msgname, 'args': y})
154             i += 1
155
156     file.close()
157     return result
158
159
160 def json2apitrace(messages, filename):
161     """Input JSON file and API message definition. Output API trace
162     bytestring."""
163
164     msgs = []
165     with open(filename, 'r') as file:
166         msgs = json.load(file, object_hook=vpp_decode)
167     result = b''
168     for m in msgs:
169         name = m['name']
170         msgobj = messages[name]
171         m['args']['_vl_msg_id'] = messages[name]._vl_msg_id
172         b = msgobj.pack(m['args'])
173
174         result += struct.pack('>I', len(b))
175         result += b
176     return len(msgs), result
177
178
179 class VPPEncoder(json.JSONEncoder):
180     def default(self, o):
181         if type(o) is bytes:
182             return "base64:" + base64.b64encode(o).decode('utf-8')
183         # Let the base class default method raise the TypeError
184         return json.JSONEncoder.default(self, o)
185
186     def encode(self, obj):
187         def hint_tuples(item):
188             if isinstance(item, tuple):
189                 return hint_tuples(item._asdict())
190             if isinstance(item, list):
191                 return [hint_tuples(e) for e in item]
192             if isinstance(item, dict):
193                 return {key: hint_tuples(value) for key, value in item.items()}
194             else:
195                 return item
196
197         return super(VPPEncoder, self).encode(hint_tuples(obj))
198
199
200 def vpp_decode(obj):
201     for k, v in obj.items():
202         if type(v) is str and v.startswith('base64:'):
203             s = v.lstrip('base64:')
204             obj[k] = base64.b64decode(v[7:])
205     return obj
206
207
208 def vpp_encoder(obj):
209     if isinstance(obj, IPv6Network):
210         return str(obj)
211     if isinstance(obj, IPv4Network):
212         return str(obj)
213     if isinstance(obj, IPv6Address):
214         return str(obj)
215     if isinstance(obj, IPv4Address):
216         return str(obj)
217     if isinstance(obj, MACAddress):
218         return str(obj)
219     if type(obj) is bytes:
220         return "base64:" + base64.b64encode(obj).decode('ascii')
221     raise TypeError('Unknown object {} {}\n'.format(type(obj), obj))
222
223 message_filter = {
224     'control_ping',
225     'memclnt_create',
226     'memclnt_delete',
227     'get_first_msg_id',
228 }
229
230 argument_filter = {
231     'client_index',
232     'context',
233 }
234
235 def topython(messages, services):
236     import pprint
237     pp = pprint.PrettyPrinter()
238
239     s = '''\
240 #!/usr/bin/env python3
241 from vpp_papi import VPP, VppEnum
242 vpp = VPP(use_socket=True)
243 vpp.connect(name='vppapitrace')
244 '''
245
246     for m in messages:
247         if m['name'] not in services:
248             s += '# ignoring reply message: {}\n'.format(m['name'])
249             continue
250         if m['name'] in message_filter:
251             s += '# ignoring message {}\n'.format(m['name'])
252             continue
253         for k in argument_filter:
254             try:
255                 m['args'].pop(k)
256             except KeyError:
257                 pass
258         a = pp.pformat(m['args'])
259         s += 'rv = vpp.api.{}(**{})\n'.format(m['name'], a)
260         s += 'print("RV:", rv)\n'
261     s += 'vpp.disconnect()\n'
262
263     return s
264
265 def todump_items(k, v, level):
266     klen = len(k) if k else 0
267     spaces = '  ' * level + ' ' * (klen + 3)
268     wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent=spaces, width=60)
269     s = ''
270     if type(v) is dict:
271         if k:
272             s += '   ' * level + '{}:\n'.format(k)
273         for k2, v2 in v.items():
274             s += todump_items(k2, v2, level + 1)
275         return s
276
277     if type(v) is list:
278         for v2 in v:
279             s += '{}'.format(todump_items(k, v2, level))
280         return s
281
282     if type(v) is bytes:
283         w = wrapper.fill(bytes.hex(v))
284         s += '   ' * level + '{}: {}\n'.format(k, w)
285     else:
286         if type(v) is str:
287             v = wrapper.fill(v)
288         s += '   ' * level + '{}: {}\n'.format(k, v)
289     return s
290
291
292 def todump(messages, services):
293     import pprint
294     pp = pprint.PrettyPrinter()
295
296     s = ''
297     for m in messages:
298         if m['name'] not in services:
299             s += '# ignoring reply message: {}\n'.format(m['name'])
300             continue
301         #if m['name'] in message_filter:
302         #    s += '# ignoring message {}\n'.format(m['name'])
303         #    continue
304         for k in argument_filter:
305             try:
306                 m['args'].pop(k)
307             except KeyError:
308                 pass
309         a = pp.pformat(m['args'])
310         s += '{}:\n'.format(m['name'])
311         s += todump_items(None, m['args'], 0)
312     return s
313
314
315 def init_api(apidir):
316     # Read API definitions
317     apifiles = VPPApiJSONFiles.find_api_files(api_dir=apidir)
318     messages = {}
319     services = {}
320     for file in apifiles:
321         with open(file) as apidef_file:
322             m, s = VPPApiJSONFiles.process_json_file(apidef_file)
323             messages.update(m)
324             services.update(s)
325     return messages, services
326
327
328 def replaymsgs(vpp, msgs):
329     for m in msgs:
330         name = m['name']
331         if name not in vpp.services:
332             continue
333         if name == 'control_ping':
334             continue
335         try:
336             m['args'].pop('client_index')
337         except KeyError:
338             pass
339         if m['args']['context'] == 0:
340             m['args']['context'] = 1
341         f = vpp.get_function(name)
342         rv = f(**m['args'])
343         print('RV {}'.format(rv))
344
345
346 def replay(args):
347     """Replay into running VPP instance"""
348
349     from vpp_papi import VPP
350
351     JSON = 1
352     APITRACE = 2
353
354     filename, file_extension = os.path.splitext(args.input)
355     input_type = JSON if file_extension == '.json' else APITRACE
356
357     vpp = VPP(use_socket=args.socket)
358     rv = vpp.connect(name='vppapireplay', chroot_prefix=args.shmprefix)
359     if rv != 0:
360         sys.exit('Cannot connect to VPP')
361
362     if input_type == JSON:
363         with open(args.input, 'r') as file:
364             msgs = json.load(file, object_hook=vpp_decode)
365     else:
366         msgs = apitrace2json(messages, args.input)
367
368     replaymsgs(vpp, msgs)
369
370     vpp.disconnect()
371
372
373 def generate(args):
374     """Generate JSON"""
375
376     JSON = 1
377     APITRACE = 2
378     PYTHON = 3
379     DUMP = 4
380
381     filename, file_extension = os.path.splitext(args.input)
382     input_type = JSON if file_extension == '.json' else APITRACE
383
384     filename, file_extension = os.path.splitext(args.output)
385     if args.todump:
386         output_type = DUMP
387     else:
388         if file_extension == '.json' or filename == '-':
389             output_type = JSON
390         elif file_extension == '.py':
391             output_type = PYTHON
392         else:
393             output_type = APITRACE
394
395     if input_type == output_type:
396         sys.exit("error: Nothing to convert between")
397
398     if input_type == JSON and output_type == APITRACE:
399         sys.exit("error: Input file must be JSON file: {}".format(args.input))
400
401     messages, services = init_api(args.apidir)
402
403     if input_type == JSON and output_type == APITRACE:
404         i = 0
405         for k, v in messages.items():
406             v._vl_msg_id = i
407             i += 1
408
409         n, result = json2apitrace(messages, args.input)
410         print('API messages: {}'.format(n))
411         header = struct.pack(">IIB", n, len(messages), 0)
412
413         i = 0
414         msgtbl = serialize_msgtbl(messages)
415         with open(args.output, 'wb') as outfile:
416             outfile.write(header)
417             outfile.write(msgtbl)
418             outfile.write(result)
419
420         return
421
422     if input_type == APITRACE:
423         result = apitrace2json(messages, args.input)
424         if output_type == PYTHON:
425             s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
426             x = json.loads(s, object_hook=vpp_decode)
427             s = topython(x, services)
428         elif output_type == DUMP:
429             s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
430             x = json.loads(s, object_hook=vpp_decode)
431             s = todump(x, services)
432         else:
433             s = json.dumps(result, cls=VPPEncoder,
434                            default=vpp_encoder, indent=4 * ' ')
435     elif output_type == PYTHON:
436         with open(args.input, 'r') as file:
437             x = json.load(file, object_hook=vpp_decode)
438             s = topython(x, services)
439     else:
440         sys.exit('Input file must be API trace file: {}'.format(args.input))
441
442     if args.output == '-':
443         sys.stdout.write(s + '\n')
444     else:
445         print('Generating {} from API trace: {}'
446               .format(args.output, args.input))
447         with open(args.output, 'w') as outfile:
448             outfile.write(s)
449
450 def general(args):
451     return
452
453 def main():
454     parser = argparse.ArgumentParser()
455     parser.add_argument('--debug', action='store_true',
456                         help='enable debug mode')
457     parser.add_argument('--apidir',
458                         help='Location of JSON API definitions')
459
460     parser.set_defaults(func=general)
461     subparsers = parser.add_subparsers(title='subcommands',
462                                        description='valid subcommands',
463                                        help='additional help')
464
465     parser_convert = subparsers.add_parser('convert',
466                                            help='Convert API trace to JSON or Python and back')
467     parser_convert.add_argument('input',
468                                 help='Input file (API trace | JSON)')
469     parser_convert.add_argument('--todump', action='store_true', help='Output text format')
470     parser_convert.add_argument('output',
471                                 help='Output file (Python | JSON | API trace)')
472     parser_convert.set_defaults(func=generate)
473
474
475     parser_replay = subparsers.add_parser('replay',
476                                           help='Replay messages to running VPP instance')
477     parser_replay.add_argument('input', help='Input file (API trace | JSON)')
478     parser_replay.add_argument('--socket', action='store_true',
479                                help='use default socket to connect to VPP')
480     parser_replay.add_argument('--shmprefix',
481                                help='connect to VPP on shared memory prefix')
482     parser_replay.set_defaults(func=replay)
483
484     args = parser.parse_args()
485     if args.debug:
486         logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
487
488     args.func(args)
489
490
491 main()