blob: 8089b3a2236e6840e2b278ab4af04c222183d414 [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
Ole Troan33a58172019-09-04 09:12:29 +0200105 # XXX 100K?
Ole Troanedfe2c02019-07-30 15:38:13 +0200106 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
117def 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
161def 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
180class 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
201def 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
209def 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
224message_filter = {
225 'control_ping',
226 'memclnt_create',
227 'memclnt_delete',
228 'get_first_msg_id',
229}
230
231argument_filter = {
232 'client_index',
233 'context',
234}
235
236def topython(messages, services):
237 import pprint
238 pp = pprint.PrettyPrinter()
239
240 s = '''\
241#!/usr/bin/env python3
242from vpp_papi import VPP, VppEnum
243vpp = VPP(use_socket=True)
244vpp.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
Ole Troana2ac36c2019-08-23 14:19:37 +0200266def 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
293def 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
Ole Troanedfe2c02019-07-30 15:38:13 +0200315
316def 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
329def 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
347def 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
374def generate(args):
375 """Generate JSON"""
376
377 JSON = 1
378 APITRACE = 2
379 PYTHON = 3
Ole Troana2ac36c2019-08-23 14:19:37 +0200380 DUMP = 4
Ole Troanedfe2c02019-07-30 15:38:13 +0200381
382 filename, file_extension = os.path.splitext(args.input)
383 input_type = JSON if file_extension == '.json' else APITRACE
Ole Troanedfe2c02019-07-30 15:38:13 +0200384 filename, file_extension = os.path.splitext(args.output)
Ole Troan33a58172019-09-04 09:12:29 +0200385
Ole Troana2ac36c2019-08-23 14:19:37 +0200386 if args.todump:
387 output_type = DUMP
Ole Troanedfe2c02019-07-30 15:38:13 +0200388 else:
Ole Troana2ac36c2019-08-23 14:19:37 +0200389 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
Ole Troanedfe2c02019-07-30 15:38:13 +0200395
396 if input_type == output_type:
397 sys.exit("error: Nothing to convert between")
398
Ole Troan33a58172019-09-04 09:12:29 +0200399 if input_type != JSON and output_type == APITRACE:
Ole Troanedfe2c02019-07-30 15:38:13 +0200400 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)
Ole Troanedfe2c02019-07-30 15:38:13 +0200411 msgtbl = serialize_msgtbl(messages)
Ole Troan33a58172019-09-04 09:12:29 +0200412
413 print('API messages: {}'.format(n))
414 header = struct.pack(">IIB", n, len(msgtbl), 0)
415
Ole Troanedfe2c02019-07-30 15:38:13 +0200416 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)
Ole Troana2ac36c2019-08-23 14:19:37 +0200429 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)
Ole Troanedfe2c02019-07-30 15:38:13 +0200433 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
451def general(args):
452 return
453
454def 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)')
Ole Troana2ac36c2019-08-23 14:19:37 +0200470 parser_convert.add_argument('--todump', action='store_true', help='Output text format')
Ole Troanedfe2c02019-07-30 15:38:13 +0200471 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()
Ole Troanedfe2c02019-07-30 15:38:13 +0200486 if args.debug:
487 logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
488
489 args.func(args)
490
491
492main()