From 36217e3ca8a1ca2e7a341b6b44ffc25e6497191c Mon Sep 17 00:00:00 2001 From: Filip Tehlar Date: Fri, 23 Jul 2021 08:51:10 +0000 Subject: api: API trace improvements Type: improvement * add support for JSON format in API trace * add ability to replay JSON API trace in both VPP and VAT2 * use CRC for backward compatibility check during JSON API replay * fix API trace CLI (and remove duplicits) * remove custom dump * remove vppapitrace.py * update docs accordingly Change-Id: I5294f68bebe6cbe738630f457f3a87720e06486b Signed-off-by: Filip Tehlar Signed-off-by: Ole Troan --- src/tools/vppapigen/vppapigen_c.py | 71 +++-- src/tools/vppapitrace/vppapitrace | 1 - src/tools/vppapitrace/vppapitrace.py | 492 ----------------------------------- 3 files changed, 53 insertions(+), 511 deletions(-) delete mode 120000 src/tools/vppapitrace/vppapitrace delete mode 100755 src/tools/vppapitrace/vppapitrace.py (limited to 'src/tools') diff --git a/src/tools/vppapigen/vppapigen_c.py b/src/tools/vppapigen/vppapigen_c.py index db684adb06d..ae47625a474 100644 --- a/src/tools/vppapigen/vppapigen_c.py +++ b/src/tools/vppapigen/vppapigen_c.py @@ -66,7 +66,10 @@ class ToJSON(): write('#ifndef included_{}_api_tojson_h\n'.format(self.module)) write('#define included_{}_api_tojson_h\n'.format(self.module)) write('#include \n\n') - write('#include \n\n') + write('#include \n\n') + if self.module == 'interface_types': + write('#define vl_printfun\n') + write('#include \n\n') def footer(self): '''Output the bottom boilerplate.''' @@ -231,6 +234,8 @@ class ToJSON(): write(' cJSON *o = cJSON_CreateObject();\n') write(' cJSON_AddStringToObject(o, "_msgname", "{}");\n' .format(o.name)) + write(' cJSON_AddStringToObject(o, "_crc", "{crc:08x}");\n' + .format(crc=o.crc)) for t in o.block: self._dispatch[t.type](self, t) @@ -312,7 +317,7 @@ class FromJSON(): write('#ifndef included_{}_api_fromjson_h\n'.format(self.module)) write('#define included_{}_api_fromjson_h\n'.format(self.module)) write('#include \n\n') - write('#include \n\n') + write('#include \n\n') write('#pragma GCC diagnostic ignored "-Wunused-label"\n') def is_base_type(self, t): @@ -338,7 +343,7 @@ class FromJSON(): if o.modern_vla: write(' char *p = cJSON_GetStringValue(item);\n') write(' size_t plen = strlen(p);\n') - write(' {msgvar} = realloc({msgvar}, {msgsize} + plen);\n' + write(' {msgvar} = cJSON_realloc({msgvar}, {msgsize} + plen, {msgsize});\n' .format(msgvar=msgvar, msgsize=msgsize)) write(' if ({msgvar} == 0) goto error;\n'.format(msgvar=msgvar)) write(' vl_api_c_string_to_api_string(p, (void *){msgvar} + ' @@ -393,7 +398,7 @@ class FromJSON(): cJSON *array = cJSON_GetObjectItem(o, "{n}"); int size = cJSON_GetArraySize(array); {lfield} = size; - {realloc} = realloc({realloc}, {msgsize} + sizeof({t}) * size); + {realloc} = cJSON_realloc({realloc}, {msgsize} + sizeof({t}) * size, {msgsize}); {t} *d = (void *){realloc} + {msgsize}; {msgsize} += sizeof({t}) * size; for (i = 0; i < size; i++) {{ @@ -419,8 +424,8 @@ class FromJSON(): write(' if (!s) goto error;\n') write(' {} = vec_len(s);\n'.format(lfield)) - write(' {realloc} = realloc({realloc}, {msgsize} + ' - 'vec_len(s));\n'.format(msgvar=msgvar, msgsize=msgsize, realloc=realloc)) + write(' {realloc} = cJSON_realloc({realloc}, {msgsize} + ' + 'vec_len(s), {msgsize});\n'.format(msgvar=msgvar, msgsize=msgsize, realloc=realloc)) write(' memcpy((void *){realloc} + {msgsize}, s, ' 'vec_len(s));\n'.format(realloc=realloc, msgsize=msgsize)) write(' {msgsize} += vec_len(s);\n'.format(msgsize=msgsize)) @@ -553,7 +558,7 @@ class FromJSON(): write(' cJSON *item __attribute__ ((unused));\n') write(' u8 *s __attribute__ ((unused));\n') write(' int l = sizeof(vl_api_{}_t);\n'.format(o.name)) - write(' vl_api_{}_t *a = malloc(l);\n'.format(o.name)) + write(' vl_api_{}_t *a = cJSON_malloc(l);\n'.format(o.name)) write('\n') for t in o.block: @@ -573,7 +578,7 @@ class FromJSON(): if error: write('\n error:\n') - write(' free(a);\n') + write(' cJSON_free(a);\n') write(' return 0;\n') write('}\n') @@ -928,10 +933,13 @@ def printfun(objs, stream, modulename): #define _uword_cast long #endif +#include "{module}.api_tojson.h" +#include "{module}.api_fromjson.h" + ''' signature = '''\ -static inline void *vl_api_{name}_t_print (vl_api_{name}_t *a, void *handle) +static inline void *vl_api_{name}_t_print{suffix} (vl_api_{name}_t *a, void *handle) {{ u8 *s = 0; u32 indent __attribute__((unused)) = 2; @@ -946,7 +954,7 @@ static inline void *vl_api_{name}_t_print (vl_api_{name}_t *a, void *handle) if t.manual_print: write("/***** manual: vl_api_%s_t_print *****/\n\n" % t.name) continue - write(signature.format(name=t.name)) + write(signature.format(name=t.name, suffix='')) write(' /* Message definition: vl_api_{}_t: */\n'.format(t.name)) write(" s = format(s, \"vl_api_%s_t:\");\n" % t.name) for o in t.block: @@ -957,6 +965,16 @@ static inline void *vl_api_{name}_t_print (vl_api_{name}_t *a, void *handle) write(' return handle;\n') write('}\n\n') + write(signature.format(name=t.name, suffix='_json')) + write(' cJSON * o = vl_api_{}_t_tojson(a);\n'.format(t.name)) + write(' (void)s;\n'); + write(' char *out = cJSON_Print(o);\n') + write(' vl_print(handle, out);\n'); + write(' cJSON_Delete(o);\n') + write(' cJSON_free(out);\n'); + write(' return handle;\n') + write('}\n\n'); + write("\n#endif") write("\n#endif /* vl_printfun */\n") @@ -1346,6 +1364,9 @@ def generate_c_boilerplate(services, defines, counters, file_crc, ' .print = vl_api_{n}_t_print,\n' ' .traced = 1,\n' ' .replay = 1,\n' + ' .print_json = vl_api_{n}_t_print_json,\n' + ' .tojson = vl_api_{n}_t_tojson,\n' + ' .fromjson = vl_api_{n}_t_fromjson,\n' ' .is_autoendian = {auto}}};\n' .format(n=s.caller, ID=s.caller.upper(), auto=d.autoendian)) @@ -1359,6 +1380,11 @@ def generate_c_boilerplate(services, defines, counters, file_crc, ' .cleanup = vl_noop_handler,\n' ' .endian = vl_api_{n}_t_endian,\n' ' .print = vl_api_{n}_t_print,\n' + ' .traced = 1,\n' + ' .replay = 1,\n' + ' .print_json = vl_api_{n}_t_print_json,\n' + ' .tojson = vl_api_{n}_t_tojson,\n' + ' .fromjson = vl_api_{n}_t_fromjson,\n' ' .is_autoendian = {auto}}};\n' .format(n=s.reply, ID=s.reply.upper(), auto=d.autoendian)) @@ -1455,7 +1481,10 @@ def generate_c_test_boilerplate(services, defines, file_crc, module, plugin, ' vl_noop_handler,\n' ' vl_api_{n}_t_endian, ' ' vl_api_{n}_t_print,\n' - ' sizeof(vl_api_{n}_t), 1);\n' + ' sizeof(vl_api_{n}_t), 1,\n' + ' vl_api_{n}_t_print_json,\n' + ' vl_api_{n}_t_tojson,\n' + ' vl_api_{n}_t_fromjson);\n' .format(n=s.reply, ID=s.reply.upper())) write(' hash_set_mem (vam->function_by_name, "{n}", api_{n});\n' .format(n=s.caller)) @@ -1474,7 +1503,10 @@ def generate_c_test_boilerplate(services, defines, file_crc, module, plugin, ' vl_noop_handler,\n' ' vl_api_{n}_t_endian, ' ' vl_api_{n}_t_print,\n' - ' sizeof(vl_api_{n}_t), 1);\n' + ' sizeof(vl_api_{n}_t), 1,\n' + ' vl_api_{n}_t_print_json,\n' + ' vl_api_{n}_t_tojson,\n' + ' vl_api_{n}_t_fromjson);\n' .format(n=e, ID=e.upper())) write('}\n') @@ -1526,7 +1558,7 @@ api_{n} (cJSON *o) mp->_vl_msg_id = vac_get_msg_index(VL_API_{N}_CRC); vl_api_{n}_t_endian(mp); vac_write((char *)mp, len); - free(mp); + cJSON_free(mp); /* Read reply */ char *p; @@ -1559,7 +1591,7 @@ api_{n} (cJSON *o) mp->_vl_msg_id = msg_id; vl_api_{n}_t_endian(mp); vac_write((char *)mp, len); - free(mp); + cJSON_free(mp); vat2_control_ping(123); // FIX CONTEXT cJSON *reply = cJSON_CreateArray(); @@ -1615,7 +1647,7 @@ api_{n} (cJSON *o) vl_api_{n}_t_endian(mp); vac_write((char *)mp, len); - free(mp); + cJSON_free(mp); cJSON *reply = cJSON_CreateArray(); @@ -1713,13 +1745,16 @@ def generate_c_test2_boilerplate(services, defines, module, stream): continue c_test_api_service(s, s.stream, stream) - write('void vat2_register_function(char *, cJSON * (*)(cJSON *), cJSON * (*)(void *));\n') + write('void vat2_register_function(char *, cJSON * (*)(cJSON *), cJSON * (*)(void *), u32);\n') # write('__attribute__((constructor))') write('clib_error_t *\n') write('vat2_register_plugin (void) {\n') for s in services: - write(' vat2_register_function("{n}", api_{n}, (cJSON * (*)(void *))vl_api_{n}_t_tojson);\n' - .format(n=s.caller)) + if s.reply not in define_hash: + continue + crc = define_hash[s.caller].crc + write(' vat2_register_function("{n}", api_{n}, (cJSON * (*)(void *))vl_api_{n}_t_tojson, 0x{crc:08x});\n' + .format(n=s.caller, crc=crc)) write(' return 0;\n') write('}\n') diff --git a/src/tools/vppapitrace/vppapitrace b/src/tools/vppapitrace/vppapitrace deleted file mode 120000 index d0ece85a809..00000000000 --- a/src/tools/vppapitrace/vppapitrace +++ /dev/null @@ -1 +0,0 @@ -vppapitrace.py \ No newline at end of file diff --git a/src/tools/vppapitrace/vppapitrace.py b/src/tools/vppapitrace/vppapitrace.py deleted file mode 100755 index 8089b3a2236..00000000000 --- a/src/tools/vppapitrace/vppapitrace.py +++ /dev/null @@ -1,492 +0,0 @@ -#!/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("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() -- cgit 1.2.3-korg