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:
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
19 # Convert from VPP API trace to JSON.
26 from ipaddress import *
27 from collections import namedtuple
28 from vpp_papi import MACAddress, VPPApiJSONFiles
33 def serialize_likely_small_unsigned_integer(x):
36 # Low bit set means it fits into 1 byte.
38 return struct.pack("B", 1 + 2 * r)
40 # Low 2 bits 1 0 means it fits into 2 bytes.
43 return struct.pack("<H", 4 * r + 2)
47 return struct.pack("<I", 8 * r + 4)
49 return struct.pack("<BQ", 0, x)
52 def unserialize_likely_small_unsigned_integer(data, offset):
53 y = struct.unpack_from("B", data, offset)[0]
58 p = struct.unpack_from("B", data, offset + 1)[0]
59 r += (y // 4) + (p << 6)
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)))
67 return struct.unpack_from(">Q", data, offset+1)[0], 8
70 def serialize_cstring(s):
71 bstring = s.encode('utf8')
73 b = serialize_likely_small_unsigned_integer(l)
74 b += struct.pack('{}s'.format(l), bstring)
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)
84 def unserialize_msgtbl(data, offset):
88 nmsg = struct.unpack_from(">I", data, offset)[0]
91 (msgid, size) = unserialize_likely_small_unsigned_integer(
94 (name, size) = unserialize_cstring(data, offset + o)
96 msgtable_by_id[msgid] = name
97 msgtable_by_name[name] = msgid
100 return msgtable_by_id, msgtable_by_name, o
103 def serialize_msgtbl(messages):
105 data = bytearray(100000)
107 data = struct.pack(">I", nmsg)
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)
116 def apitrace2json(messages, filename):
118 with open(filename, 'rb') as file:
119 bytes_read = file.read()
121 (nitems, msgtbl_size, wrapped) = struct.unpack_from(">IIB",
123 logging.debug('nitems: {} message table size: {} wrapped: {}'
124 .format(nitems, msgtbl_size, wrapped))
126 sys.stdout.write('Wrapped/incomplete trace, results may vary')
129 msgtbl_by_id, msgtbl_by_name, size = unserialize_msgtbl(bytes_read,
135 size = struct.unpack_from(">I", bytes_read, offset)[0]
139 msgid = struct.unpack_from(">H", bytes_read, offset)[0]
140 name = msgtbl_by_id[msgid]
141 n = name[:name.rfind("_")]
143 if n + '_' + msgobj.crc[2:] != name:
144 sys.exit("CRC Mismatch between JSON API definition "
145 "and trace. {}".format(name))
147 x, s = msgobj.unpack(bytes_read[offset:offset+size])
148 msgname = type(x).__name__
150 # Replace named tuple illegal _0
153 result.append({'name': msgname, 'args': y})
160 def json2apitrace(messages, filename):
161 """Input JSON file and API message definition. Output API trace
165 with open(filename, 'r') as file:
166 msgs = json.load(file, object_hook=vpp_decode)
170 msgobj = messages[name]
171 m['args']['_vl_msg_id'] = messages[name]._vl_msg_id
172 b = msgobj.pack(m['args'])
174 result += struct.pack('>I', len(b))
176 return len(msgs), result
179 class VPPEncoder(json.JSONEncoder):
180 def default(self, o):
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)
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()}
197 return super(VPPEncoder, self).encode(hint_tuples(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:])
208 def vpp_encoder(obj):
209 if isinstance(obj, IPv6Network):
211 if isinstance(obj, IPv4Network):
213 if isinstance(obj, IPv6Address):
215 if isinstance(obj, IPv4Address):
217 if isinstance(obj, MACAddress):
219 if type(obj) is bytes:
220 return "base64:" + base64.b64encode(obj).decode('ascii')
221 raise TypeError('Unknown object {} {}\n'.format(type(obj), obj))
235 def topython(messages, services):
237 pp = pprint.PrettyPrinter()
240 #!/usr/bin/env python3
241 from vpp_papi import VPP, VppEnum
242 vpp = VPP(use_socket=True)
243 vpp.connect(name='vppapitrace')
247 if m['name'] not in services:
248 s += '# ignoring reply message: {}\n'.format(m['name'])
250 if m['name'] in message_filter:
251 s += '# ignoring message {}\n'.format(m['name'])
253 for k in argument_filter:
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'
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)
272 s += ' ' * level + '{}:\n'.format(k)
273 for k2, v2 in v.items():
274 s += todump_items(k2, v2, level + 1)
279 s += '{}'.format(todump_items(k, v2, level))
283 w = wrapper.fill(bytes.hex(v))
284 s += ' ' * level + '{}: {}\n'.format(k, w)
288 s += ' ' * level + '{}: {}\n'.format(k, v)
292 def todump(messages, services):
294 pp = pprint.PrettyPrinter()
298 if m['name'] not in services:
299 s += '# ignoring reply message: {}\n'.format(m['name'])
301 #if m['name'] in message_filter:
302 # s += '# ignoring message {}\n'.format(m['name'])
304 for k in argument_filter:
309 a = pp.pformat(m['args'])
310 s += '{}:\n'.format(m['name'])
311 s += todump_items(None, m['args'], 0)
315 def init_api(apidir):
316 # Read API definitions
317 apifiles = VPPApiJSONFiles.find_api_files(api_dir=apidir)
320 for file in apifiles:
321 with open(file) as apidef_file:
322 m, s = VPPApiJSONFiles.process_json_file(apidef_file)
325 return messages, services
328 def replaymsgs(vpp, msgs):
331 if name not in vpp.services:
333 if name == 'control_ping':
336 m['args'].pop('client_index')
339 if m['args']['context'] == 0:
340 m['args']['context'] = 1
341 f = vpp.get_function(name)
343 print('RV {}'.format(rv))
347 """Replay into running VPP instance"""
349 from vpp_papi import VPP
354 filename, file_extension = os.path.splitext(args.input)
355 input_type = JSON if file_extension == '.json' else APITRACE
357 vpp = VPP(use_socket=args.socket)
358 rv = vpp.connect(name='vppapireplay', chroot_prefix=args.shmprefix)
360 sys.exit('Cannot connect to VPP')
362 if input_type == JSON:
363 with open(args.input, 'r') as file:
364 msgs = json.load(file, object_hook=vpp_decode)
366 msgs = apitrace2json(messages, args.input)
368 replaymsgs(vpp, msgs)
381 filename, file_extension = os.path.splitext(args.input)
382 input_type = JSON if file_extension == '.json' else APITRACE
384 filename, file_extension = os.path.splitext(args.output)
388 if file_extension == '.json' or filename == '-':
390 elif file_extension == '.py':
393 output_type = APITRACE
395 if input_type == output_type:
396 sys.exit("error: Nothing to convert between")
398 if input_type == JSON and output_type == APITRACE:
399 sys.exit("error: Input file must be JSON file: {}".format(args.input))
401 messages, services = init_api(args.apidir)
403 if input_type == JSON and output_type == APITRACE:
405 for k, v in messages.items():
409 n, result = json2apitrace(messages, args.input)
410 print('API messages: {}'.format(n))
411 header = struct.pack(">IIB", n, len(messages), 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)
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)
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)
440 sys.exit('Input file must be API trace file: {}'.format(args.input))
442 if args.output == '-':
443 sys.stdout.write(s + '\n')
445 print('Generating {} from API trace: {}'
446 .format(args.output, args.input))
447 with open(args.output, 'w') as outfile:
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')
460 parser.set_defaults(func=general)
461 subparsers = parser.add_subparsers(title='subcommands',
462 description='valid subcommands',
463 help='additional help')
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)
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)
484 args = parser.parse_args()
486 logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)