blob: 8089b3a2236e6840e2b278ab4af04c222183d414 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (c) 2019 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# Convert from VPP API trace to JSON.
import argparse
import struct
import sys
import logging
import json
from ipaddress import *
from collections import namedtuple
from vpp_papi import MACAddress, VPPApiJSONFiles
import base64
import os
import textwrap
def serialize_likely_small_unsigned_integer(x):
r = x
# Low bit set means it fits into 1 byte.
if r < (1 << 7):
return struct.pack("B", 1 + 2 * r)
# Low 2 bits 1 0 means it fits into 2 bytes.
r -= (1 << 7)
if r < (1 << 14):
return struct.pack("<H", 4 * r + 2)
r -= (1 << 14)
if r < (1 << 29):
return struct.pack("<I", 8 * r + 4)
return struct.pack("<BQ", 0, x)
def unserialize_likely_small_unsigned_integer(data, offset):
y = struct.unpack_from("B", data, offset)[0]
if y & 1:
return y // 2, 1
r = 1 << 7
if y & 2:
p = struct.unpack_from("B", data, offset + 1)[0]
r += (y // 4) + (p << 6)
return r, 2
r += 1 << 14
if y & 4:
(p1, p2, p3) = struct.unpack_from("BBB", data, offset+1)
r += ((y // 8) + (p1 << (5 + 8 * 0))
+ (p2 << (5 + 8 * 1)) + (p3 << (5 + 8 * 2)))
return r, 3
return struct.unpack_from(">Q", data, offset+1)[0], 8
def serialize_cstring(s):
bstring = s.encode('utf8')
l = len(bstring)
b = serialize_likely_small_unsigned_integer(l)
b += struct.pack('{}s'.format(l), bstring)
return b
def unserialize_cstring(data, offset):
l, size = unserialize_likely_small_unsigned_integer(data, offset)
name = struct.unpack_from('{}s'.format(l), data, offset+size)[0]
return name.decode('utf8'), size + len(name)
def unserialize_msgtbl(data, offset):
msgtable_by_id = {}
msgtable_by_name = {}
i = 0
nmsg = struct.unpack_from(">I", data, offset)[0]
o = 4
while i < nmsg:
(msgid, size) = unserialize_likely_small_unsigned_integer(
data, offset + o)
o += size
(name, size) = unserialize_cstring(data, offset + o)
o += size
msgtable_by_id[msgid] = name
msgtable_by_name[name] = msgid
i += 1
return msgtable_by_id, msgtable_by_name, o
def serialize_msgtbl(messages):
offset = 0
# XXX 100K?
data = bytearray(100000)
nmsg = len(messages)
data = struct.pack(">I", nmsg)
for k, v in messages.items():
name = k + '_' + v.crc[2:]
data += serialize_likely_small_unsigned_integer(v._vl_msg_id)
data += serialize_cstring(name)
return data
def apitrace2json(messages, filename):
result = []
with open(filename, 'rb') as file:
bytes_read = file.read()
# Read header
(nitems, msgtbl_size, wrapped) = struct.unpack_from(">IIB",
bytes_read, 0)
logging.debug('nitems: {} message table size: {} wrapped: {}'
.format(nitems, msgtbl_size, wrapped))
if wrapped:
sys.stdout.write('Wrapped/incomplete trace, results may vary')
offset = 9
msgtbl_by_id, msgtbl_by_name, size = unserialize_msgtbl(bytes_read,
offset)
offset += size
i = 0
while i < nitems:
size = struct.unpack_from(">I", bytes_read, offset)[0]
offset += 4
if size == 0:
break
msgid = struct.unpack_from(">H", bytes_read, offset)[0]
name = msgtbl_by_id[msgid]
n = name[:name.rfind("_")]
msgobj = messages[n]
if n + '_' + msgobj.crc[2:] != name:
sys.exit("CRC Mismatch between JSON API definition "
"and trace. {}".format(name))
x, s = msgobj.unpack(bytes_read[offset:offset+size])
msgname = type(x).__name__
offset += size
# Replace named tuple illegal _0
y = x._asdict()
y.pop('_0')
result.append({'name': msgname, 'args': y})
i += 1
file.close()
return result
def json2apitrace(messages, filename):
"""Input JSON file and API message definition. Output API trace
bytestring."""
msgs = []
with open(filename, 'r') as file:
msgs = json.load(file, object_hook=vpp_decode)
result = b''
for m in msgs:
name = m['name']
msgobj = messages[name]
m['args']['_vl_msg_id'] = messages[name]._vl_msg_id
b = msgobj.pack(m['args'])
result += struct.pack('>I', len(b))
result += b
return len(msgs), result
class VPPEncoder(json.JSONEncoder):
def default(self, o):
if type(o) is bytes:
return "base64:" + base64.b64encode(o).decode('utf-8')
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, o)
def encode(self, obj):
def hint_tuples(item):
if isinstance(item, tuple):
return hint_tuples(item._asdict())
if isinstance(item, list):
return [hint_tuples(e) for e in item]
if isinstance(item, dict):
return {key: hint_tuples(value) for key, value in item.items()}
else:
return item
return super(VPPEncoder, self).encode(hint_tuples(obj))
def vpp_decode(obj):
for k, v in obj.items():
if type(v) is str and v.startswith('base64:'):
s = v.lstrip('base64:')
obj[k] = base64.b64decode(v[7:])
return obj
def vpp_encoder(obj):
if isinstance(obj, IPv6Network):
return str(obj)
if isinstance(obj, IPv4Network):
return str(obj)
if isinstance(obj, IPv6Address):
return str(obj)
if isinstance(obj, IPv4Address):
return str(obj)
if isinstance(obj, MACAddress):
return str(obj)
if type(obj) is bytes:
return "base64:" + base64.b64encode(obj).decode('ascii')
raise TypeError('Unknown object {} {}\n'.format(type(obj), obj))
message_filter = {
'control_ping',
'memclnt_create',
'memclnt_delete',
'get_first_msg_id',
}
argument_filter = {
'client_index',
'context',
}
def topython(messages, services):
import pprint
pp = pprint.PrettyPrinter()
s = '''\
#!/usr/bin/env python3
from vpp_papi import VPP, VppEnum
vpp = VPP(use_socket=True)
vpp.connect(name='vppapitrace')
'''
for m in messages:
if m['name'] not in services:
s += '# ignoring reply message: {}\n'.format(m['name'])
continue
if m['name'] in message_filter:
s += '# ignoring message {}\n'.format(m['name'])
continue
for k in argument_filter:
try:
m['args'].pop(k)
except KeyError:
pass
a = pp.pformat(m['args'])
s += 'rv = vpp.api.{}(**{})\n'.format(m['name'], a)
s += 'print("RV:", rv)\n'
s += 'vpp.disconnect()\n'
return s
def todump_items(k, v, level):
klen = len(k) if k else 0
spaces = ' ' * level + ' ' * (klen + 3)
wrapper = textwrap.TextWrapper(initial_indent="", subsequent_indent=spaces, width=60)
s = ''
if type(v) is dict:
if k:
s += ' ' * level + '{}:\n'.format(k)
for k2, v2 in v.items():
s += todump_items(k2, v2, level + 1)
return s
if type(v) is list:
for v2 in v:
s += '{}'.format(todump_items(k, v2, level))
return s
if type(v) is bytes:
w = wrapper.fill(bytes.hex(v))
s += ' ' * level + '{}: {}\n'.format(k, w)
else:
if type(v) is str:
v = wrapper.fill(v)
s += ' ' * level + '{}: {}\n'.format(k, v)
return s
def todump(messages, services):
import pprint
pp = pprint.PrettyPrinter()
s = ''
for m in messages:
if m['name'] not in services:
s += '# ignoring reply message: {}\n'.format(m['name'])
continue
#if m['name'] in message_filter:
# s += '# ignoring message {}\n'.format(m['name'])
# continue
for k in argument_filter:
try:
m['args'].pop(k)
except KeyError:
pass
a = pp.pformat(m['args'])
s += '{}:\n'.format(m['name'])
s += todump_items(None, m['args'], 0)
return s
def init_api(apidir):
# Read API definitions
apifiles = VPPApiJSONFiles.find_api_files(api_dir=apidir)
messages = {}
services = {}
for file in apifiles:
with open(file) as apidef_file:
m, s = VPPApiJSONFiles.process_json_file(apidef_file)
messages.update(m)
services.update(s)
return messages, services
def replaymsgs(vpp, msgs):
for m in msgs:
name = m['name']
if name not in vpp.services:
continue
if name == 'control_ping':
continue
try:
m['args'].pop('client_index')
except KeyError:
pass
if m['args']['context'] == 0:
m['args']['context'] = 1
f = vpp.get_function(name)
rv = f(**m['args'])
print('RV {}'.format(rv))
def replay(args):
"""Replay into running VPP instance"""
from vpp_papi import VPP
JSON = 1
APITRACE = 2
filename, file_extension = os.path.splitext(args.input)
input_type = JSON if file_extension == '.json' else APITRACE
vpp = VPP(use_socket=args.socket)
rv = vpp.connect(name='vppapireplay', chroot_prefix=args.shmprefix)
if rv != 0:
sys.exit('Cannot connect to VPP')
if input_type == JSON:
with open(args.input, 'r') as file:
msgs = json.load(file, object_hook=vpp_decode)
else:
msgs = apitrace2json(messages, args.input)
replaymsgs(vpp, msgs)
vpp.disconnect()
def generate(args):
"""Generate JSON"""
JSON = 1
APITRACE = 2
PYTHON = 3
DUMP = 4
filename, file_extension = os.path.splitext(args.input)
input_type = JSON if file_extension == '.json' else APITRACE
filename, file_extension = os.path.splitext(args.output)
if args.todump:
output_type = DUMP
else:
if file_extension == '.json' or filename == '-':
output_type = JSON
elif file_extension == '.py':
output_type = PYTHON
else:
output_type = APITRACE
if input_type == output_type:
sys.exit("error: Nothing to convert between")
if input_type != JSON and output_type == APITRACE:
sys.exit("error: Input file must be JSON file: {}".format(args.input))
messages, services = init_api(args.apidir)
if input_type == JSON and output_type == APITRACE:
i = 0
for k, v in messages.items():
v._vl_msg_id = i
i += 1
n, result = json2apitrace(messages, args.input)
msgtbl = serialize_msgtbl(messages)
print('API messages: {}'.format(n))
header = struct.pack(">IIB", n, len(msgtbl), 0)
with open(args.output, 'wb') as outfile:
outfile.write(header)
outfile.write(msgtbl)
outfile.write(result)
return
if input_type == APITRACE:
result = apitrace2json(messages, args.input)
if output_type == PYTHON:
s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
x = json.loads(s, object_hook=vpp_decode)
s = topython(x, services)
elif output_type == DUMP:
s = json.dumps(result, cls=VPPEncoder, default=vpp_encoder)
x = json.loads(s, object_hook=vpp_decode)
s = todump(x, services)
else:
s = json.dumps(result, cls=VPPEncoder,
default=vpp_encoder, indent=4 * ' ')
elif output_type == PYTHON:
with open(args.input, 'r') as file:
x = json.load(file, object_hook=vpp_decode)
s = topython(x, services)
else:
sys.exit('Input file must be API trace file: {}'.format(args.input))
if args.output == '-':
sys.stdout.write(s + '\n')
else:
print('Generating {} from API trace: {}'
.format(args.output, args.input))
with open(args.output, 'w') as outfile:
outfile.write(s)
def general(args):
return
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--debug', action='store_true',
help='enable debug mode')
parser.add_argument('--apidir',
help='Location of JSON API definitions')
parser.set_defaults(func=general)
subparsers = parser.add_subparsers(title='subcommands',
description='valid subcommands',
help='additional help')
parser_convert = subparsers.add_parser('convert',
help='Convert API trace to JSON or Python and back')
parser_convert.add_argument('input',
help='Input file (API trace | JSON)')
parser_convert.add_argument('--todump', action='store_true', help='Output text format')
parser_convert.add_argument('output',
help='Output file (Python | JSON | API trace)')
parser_convert.set_defaults(func=generate)
parser_replay = subparsers.add_parser('replay',
help='Replay messages to running VPP instance')
parser_replay.add_argument('input', help='Input file (API trace | JSON)')
parser_replay.add_argument('--socket', action='store_true',
help='use default socket to connect to VPP')
parser_replay.add_argument('--shmprefix',
help='connect to VPP on shared memory prefix')
parser_replay.set_defaults(func=replay)
args = parser.parse_args()
if args.debug:
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
args.func(args)
main()