blob: 640b820d994362c43feaa015e61a15e7c26abf14 [file] [log] [blame]
Ole Troanedfe2c02019-07-30 15:38:13 +02001#!/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
21import argparse
22import struct
23import sys
24import logging
25import json
26from ipaddress import *
27from collections import namedtuple
28from vpp_papi import MACAddress, VPPApiJSONFiles
29import base64
30import os
Ole Troana2ac36c2019-08-23 14:19:37 +020031import textwrap
Ole Troanedfe2c02019-07-30 15:38:13 +020032
33def 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
52def 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
70def serialize_cstring(s):
71 bstring = s.encode('utf8')
72 l = len(bstring)
Ole Troana2ac36c2019-08-23 14:19:37 +020073 b = serialize_likely_small_unsigned_integer(l)
Ole Troanedfe2c02019-07-30 15:38:13 +020074 b += struct.pack('{}s'.format(l), bstring)
75 return b
76
77
78def 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
84def 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
103def 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
116def 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
160def 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
179class 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
200def 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
208def 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
223message_filter = {
224 'control_ping',
225 'memclnt_create',
226 'memclnt_delete',
227 'get_first_msg_id',
228}
229
230argument_filter = {
231 'client_index',
232 'context',
233}
234
235def topython(messages, services):
236 import pprint
237 pp = pprint.PrettyPrinter()
238
239 s = '''\
240#!/usr/bin/env python3
241from vpp_papi import VPP, VppEnum
242vpp = VPP(use_socket=True)
243vpp.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
Ole Troana2ac36c2019-08-23 14:19:37 +0200265def 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
292def 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
Ole Troanedfe2c02019-07-30 15:38:13 +0200314
315def 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
328def 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
346def 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
373def generate(args):
374 """Generate JSON"""
375
376 JSON = 1
377 APITRACE = 2
378 PYTHON = 3
Ole Troana2ac36c2019-08-23 14:19:37 +0200379 DUMP = 4
Ole Troanedfe2c02019-07-30 15:38:13 +0200380
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)
Ole Troana2ac36c2019-08-23 14:19:37 +0200385 if args.todump:
386 output_type = DUMP
Ole Troanedfe2c02019-07-30 15:38:13 +0200387 else:
Ole Troana2ac36c2019-08-23 14:19:37 +0200388 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
Ole Troanedfe2c02019-07-30 15:38:13 +0200394
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)
Ole Troana2ac36c2019-08-23 14:19:37 +0200428 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)
Ole Troanedfe2c02019-07-30 15:38:13 +0200432 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
450def general(args):
451 return
452
453def 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)')
Ole Troana2ac36c2019-08-23 14:19:37 +0200469 parser_convert.add_argument('--todump', action='store_true', help='Output text format')
Ole Troanedfe2c02019-07-30 15:38:13 +0200470 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()
Ole Troanedfe2c02019-07-30 15:38:13 +0200485 if args.debug:
486 logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
487
488 args.func(args)
489
490
491main()