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'
266 def init_api(apidir):
267 # Read API definitions
268 apifiles = VPPApiJSONFiles.find_api_files(api_dir=apidir)
271 for file in apifiles:
272 with open(file) as apidef_file:
273 m, s = VPPApiJSONFiles.process_json_file(apidef_file)
276 return messages, services
279 def replaymsgs(vpp, msgs):
282 if name not in vpp.services:
284 if name == 'control_ping':
287 m['args'].pop('client_index')
290 if m['args']['context'] == 0:
291 m['args']['context'] = 1
292 f = vpp.get_function(name)
294 print('RV {}'.format(rv))
298 """Replay into running VPP instance"""
300 from vpp_papi import VPP
305 filename, file_extension = os.path.splitext(args.input)
306 input_type = JSON if file_extension == '.json' else APITRACE
308 vpp = VPP(use_socket=args.socket)
309 rv = vpp.connect(name='vppapireplay', chroot_prefix=args.shmprefix)
311 sys.exit('Cannot connect to VPP')
313 if input_type == JSON:
314 with open(args.input, 'r') as file:
315 msgs = json.load(file, object_hook=vpp_decode)
317 msgs = apitrace2json(messages, args.input)
319 replaymsgs(vpp, msgs)
331 filename, file_extension = os.path.splitext(args.input)
332 input_type = JSON if file_extension == '.json' else APITRACE
334 filename, file_extension = os.path.splitext(args.output)
335 if file_extension == '.json' or filename == '-':
337 elif file_extension == '.py':
340 output_type = APITRACE
342 if input_type == output_type:
343 sys.exit("error: Nothing to convert between")
345 if input_type == JSON and output_type == APITRACE:
346 sys.exit("error: Input file must be JSON file: {}".format(args.input))
348 messages, services = init_api(args.apidir)
350 if input_type == JSON and output_type == APITRACE:
352 for k, v in messages.items():
356 n, result = json2apitrace(messages, args.input)
357 print('API messages: {}'.format(n))
358 header = struct.pack(">IIB", n, len(messages), 0)
361 msgtbl = serialize_msgtbl(messages)
362 with open(args.output, 'wb') as outfile:
363 outfile.write(header)
364 outfile.write(msgtbl)
365 outfile.write(result)
369 if input_type == APITRACE:
370 result = apitrace2json(messages, args.input)
371 if output_type == PYTHON:
372 s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
373 x = json.loads(s, object_hook=vpp_decode)
374 s = topython(x, services)
376 s = json.dumps(result, cls=VPPEncoder,
377 default=vpp_encoder, indent=4 * ' ')
378 elif output_type == PYTHON:
379 with open(args.input, 'r') as file:
380 x = json.load(file, object_hook=vpp_decode)
381 s = topython(x, services)
383 sys.exit('Input file must be API trace file: {}'.format(args.input))
385 if args.output == '-':
386 sys.stdout.write(s + '\n')
388 print('Generating {} from API trace: {}'
389 .format(args.output, args.input))
390 with open(args.output, 'w') as outfile:
397 parser = argparse.ArgumentParser()
398 parser.add_argument('--debug', action='store_true',
399 help='enable debug mode')
400 parser.add_argument('--apidir',
401 help='Location of JSON API definitions')
403 parser.set_defaults(func=general)
404 subparsers = parser.add_subparsers(title='subcommands',
405 description='valid subcommands',
406 help='additional help')
408 parser_convert = subparsers.add_parser('convert',
409 help='Convert API trace to JSON or Python and back')
410 parser_convert.add_argument('input',
411 help='Input file (API trace | JSON)')
412 parser_convert.add_argument('output',
413 help='Output file (Python | JSON | API trace)')
414 parser_convert.set_defaults(func=generate)
417 parser_replay = subparsers.add_parser('replay',
418 help='Replay messages to running VPP instance')
419 parser_replay.add_argument('input', help='Input file (API trace | JSON)')
420 parser_replay.add_argument('--socket', action='store_true',
421 help='use default socket to connect to VPP')
422 parser_replay.add_argument('--shmprefix',
423 help='connect to VPP on shared memory prefix')
424 parser_replay.set_defaults(func=replay)
426 args = parser.parse_args()
429 logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)