api: provide api definition over api
This patch allows a client to bootstrap itself by downloading the JSON API definitions over the API itself. This patch enables it for Python (probably need a dynamic language). Call VPPApiClient with the new bootstrapapi=True parameter. Example (Python): from vpp_papi import VPPApiClient vpp = VPPApiClient(bootstrapapi=True) rv = vpp.connect("foobar") assert rv == 0 print(f'SHOW VERSION: {vpp.api.show_version()}') vpp.disconnect() Type: feature Change-Id: Id903fdccc82b2e22aa1994331d2c150253f2ccae Signed-off-by: Ole Troan <otroan@employees.org>
This commit is contained in:

committed by
Andrew Yourtchenko

parent
f34b6800de
commit
ac0babd412
@ -64,7 +64,7 @@ function(vpp_generate_api_json_header file dir component)
|
||||
add_custom_command (OUTPUT ${output_name}
|
||||
COMMAND mkdir -p ${output_dir}
|
||||
COMMAND ${PYENV} ${VPP_APIGEN}
|
||||
ARGS ${includedir} --includedir ${CMAKE_SOURCE_DIR} --input ${CMAKE_CURRENT_SOURCE_DIR}/${file} JSON --output ${output_name}
|
||||
ARGS ${includedir} --includedir ${CMAKE_SOURCE_DIR} --input ${CMAKE_CURRENT_SOURCE_DIR}/${file} JSON --outputdir ${output_dir} --output ${output_name}
|
||||
DEPENDS ${VPP_APIGEN} ${CMAKE_CURRENT_SOURCE_DIR}/${file}
|
||||
COMMENT "Generating API header ${output_name}"
|
||||
)
|
||||
|
@ -110,6 +110,15 @@ def main():
|
||||
],
|
||||
f.name,
|
||||
),
|
||||
"outputdir": "%s/%s/"
|
||||
% (
|
||||
output_path,
|
||||
output_dir_map[
|
||||
f.as_posix().split("/")[
|
||||
src_dir_depth + BASE_DIR.count("/") - 1
|
||||
]
|
||||
],
|
||||
),
|
||||
"input_file": f.as_posix(),
|
||||
"includedir": [src_dir.as_posix()],
|
||||
"output_module": "JSON",
|
||||
|
@ -1574,6 +1574,7 @@ def generate_c_boilerplate(services, defines, counters, file_crc, module, stream
|
||||
#include "{module}.api.h"
|
||||
#undef vl_printfun
|
||||
|
||||
#include "{module}.api_json.h"
|
||||
"""
|
||||
|
||||
write(hdr.format(module=module))
|
||||
@ -1586,6 +1587,7 @@ def generate_c_boilerplate(services, defines, counters, file_crc, module, stream
|
||||
' u16 msg_id_base = vl_msg_api_get_msg_ids ("{}_{crc:08x}", '
|
||||
"VL_MSG_{m}_LAST);\n".format(module, crc=file_crc, m=module.upper())
|
||||
)
|
||||
write(f" vec_add1(am->json_api_repr, (u8 *)json_api_repr_{module});\n")
|
||||
|
||||
for d in defines:
|
||||
write(
|
||||
|
@ -1,5 +1,7 @@
|
||||
# JSON generation
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
process_imports = True
|
||||
|
||||
@ -88,7 +90,26 @@ def walk_defs(s, is_message=False):
|
||||
#
|
||||
# Plugin entry point
|
||||
#
|
||||
def run(output_dir, filename, s):
|
||||
|
||||
|
||||
def contents_to_c_string(contents):
|
||||
# Escape backslashes and double quotes
|
||||
contents = contents.replace("\\", "\\\\").replace('"', '\\"')
|
||||
# Replace newlines with \n
|
||||
contents = contents.replace("\n", "\\n")
|
||||
return '"' + contents + '"'
|
||||
|
||||
|
||||
def run(output_dir, apifilename, s):
|
||||
if not output_dir:
|
||||
sys.stderr.write("Missing --outputdir argument")
|
||||
return None
|
||||
|
||||
basename = os.path.basename(apifilename)
|
||||
filename_json_repr = os.path.join(output_dir + "/" + basename + "_json.h")
|
||||
filename, _ = os.path.splitext(basename)
|
||||
modulename = filename.replace(".", "_")
|
||||
|
||||
j = {}
|
||||
|
||||
j["types"] = walk_defs([o for o in s["types"] if o.__class__.__name__ == "Typedef"])
|
||||
@ -106,4 +127,9 @@ def run(output_dir, filename, s):
|
||||
j["vl_api_version"] = hex(s["file_crc"])
|
||||
j["imports"] = walk_imports(i for i in s["Import"])
|
||||
j["counters"], j["paths"] = walk_counters(s["Counters"], s["Paths"])
|
||||
return json.dumps(j, indent=4, separators=(",", ": "))
|
||||
r = json.dumps(j, indent=4, separators=(",", ": "))
|
||||
c_string = contents_to_c_string(r)
|
||||
with open(filename_json_repr, "w", encoding="UTF-8") as f:
|
||||
print(f"const char *json_api_repr_{modulename} = {c_string};", file=f)
|
||||
# return json.dumps(j, indent=4, separators=(",", ": "))
|
||||
return r
|
||||
|
@ -354,6 +354,8 @@ typedef struct api_main_t
|
||||
|
||||
/** client message index hash table */
|
||||
uword *msg_index_by_name_and_crc;
|
||||
/** plugin JSON representation vector table */
|
||||
u8 **json_api_repr;
|
||||
|
||||
/** api version list */
|
||||
api_version_t *api_version_list;
|
||||
|
@ -252,3 +252,14 @@ define memclnt_create_v2_reply {
|
||||
u32 index; /* index, used e.g. by API trace replay */
|
||||
u64 message_table; /* serialized message table in shmem */
|
||||
};
|
||||
|
||||
define get_api_json {
|
||||
u32 client_index;
|
||||
u32 context;
|
||||
};
|
||||
|
||||
define get_api_json_reply {
|
||||
u32 context;
|
||||
i32 retval;
|
||||
string json[];
|
||||
};
|
||||
|
@ -145,10 +145,44 @@ vl_api_control_ping_t_handler (vl_api_control_ping_t *mp)
|
||||
({ rmp->vpe_pid = ntohl (getpid ()); }));
|
||||
}
|
||||
|
||||
static void
|
||||
vl_api_get_api_json_t_handler (vl_api_get_api_json_t *mp)
|
||||
{
|
||||
vl_api_get_api_json_reply_t *rmp;
|
||||
api_main_t *am = vlibapi_get_main ();
|
||||
int rv = 0, n = 0;
|
||||
u8 *s = 0;
|
||||
|
||||
vl_api_registration_t *rp =
|
||||
vl_api_client_index_to_registration (mp->client_index);
|
||||
if (rp == 0)
|
||||
return;
|
||||
|
||||
s = format (s, "[\n");
|
||||
u8 **ptr;
|
||||
vec_foreach (ptr, am->json_api_repr)
|
||||
{
|
||||
s = format (s, "%s,", ptr[0]);
|
||||
}
|
||||
s[vec_len (s) - 1] = ']'; // Replace last comma with a bracket
|
||||
vec_terminate_c_string (s);
|
||||
n = vec_len (s);
|
||||
|
||||
done:
|
||||
REPLY_MACRO3 (VL_API_GET_API_JSON_REPLY, n, ({
|
||||
if (rv == 0)
|
||||
{
|
||||
vl_api_c_string_to_api_string ((char *) s, &rmp->json);
|
||||
}
|
||||
}));
|
||||
vec_free (s);
|
||||
}
|
||||
|
||||
#define foreach_vlib_api_msg \
|
||||
_ (GET_FIRST_MSG_ID, get_first_msg_id) \
|
||||
_ (API_VERSIONS, api_versions) \
|
||||
_ (CONTROL_PING, control_ping)
|
||||
_ (CONTROL_PING, control_ping) \
|
||||
_ (GET_API_JSON, get_api_json)
|
||||
|
||||
/*
|
||||
* vl_api_init
|
||||
|
@ -11,7 +11,6 @@
|
||||
# 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.
|
||||
import sys
|
||||
|
||||
try:
|
||||
from setuptools import setup, find_packages
|
||||
@ -22,7 +21,7 @@ requirements = []
|
||||
|
||||
setup(
|
||||
name="vpp_papi",
|
||||
version="2.0.0",
|
||||
version="2.1.0",
|
||||
description="VPP Python binding",
|
||||
author="Ole Troan",
|
||||
author_email="ot@cisco.com",
|
||||
@ -31,6 +30,7 @@ setup(
|
||||
test_suite="vpp_papi.tests",
|
||||
install_requires=requirements,
|
||||
packages=find_packages(),
|
||||
package_data={"vpp_papi": ["data/*.json"]},
|
||||
long_description="""VPP Python language binding.""",
|
||||
zip_safe=True,
|
||||
)
|
||||
|
809
src/vpp-api/python/vpp_papi/data/memclnt.api.json
Normal file
809
src/vpp-api/python/vpp_papi/data/memclnt.api.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,6 @@ from __future__ import print_function
|
||||
from __future__ import absolute_import
|
||||
import ctypes
|
||||
import ipaddress
|
||||
import sys
|
||||
import multiprocessing as mp
|
||||
import os
|
||||
import queue
|
||||
@ -30,6 +29,7 @@ import fnmatch
|
||||
import weakref
|
||||
import atexit
|
||||
import time
|
||||
import pkg_resources
|
||||
from .vpp_format import verify_enum_hint
|
||||
from .vpp_serializer import VPPType, VPPEnumType, VPPEnumFlagType, VPPUnionType
|
||||
from .vpp_serializer import VPPMessage, vpp_get_type, VPPTypeAlias
|
||||
@ -281,15 +281,28 @@ class VPPApiJSONFiles:
|
||||
|
||||
@classmethod
|
||||
def process_json_file(self, apidef_file):
|
||||
return self._process_json(apidef_file.read())
|
||||
api = json.load(apidef_file)
|
||||
return self._process_json(api)
|
||||
|
||||
@classmethod
|
||||
def process_json_str(self, json_str):
|
||||
return self._process_json(json_str)
|
||||
api = json.loads(json_str)
|
||||
return self._process_json(api)
|
||||
|
||||
@classmethod
|
||||
def process_json_array_str(self, json_str):
|
||||
services = {}
|
||||
messages = {}
|
||||
|
||||
apis = json.loads(json_str)
|
||||
for a in apis:
|
||||
m, s = self._process_json(a)
|
||||
messages.update(m)
|
||||
services.update(s)
|
||||
return messages, services
|
||||
|
||||
@staticmethod
|
||||
def _process_json(json_str): # -> Tuple[Dict, Dict]
|
||||
api = json.loads(json_str)
|
||||
def _process_json(api): # -> Tuple[Dict, Dict]
|
||||
types = {}
|
||||
services = {}
|
||||
messages = {}
|
||||
@ -373,7 +386,6 @@ class VPPApiJSONFiles:
|
||||
try:
|
||||
messages[m[0]] = VPPMessage(m[0], m[1:])
|
||||
except VPPNotImplementedError:
|
||||
### OLE FIXME
|
||||
logger.error("Not implemented error for {}".format(m[0]))
|
||||
except KeyError:
|
||||
pass
|
||||
@ -435,6 +447,7 @@ class VPPApiClient:
|
||||
read_timeout=5,
|
||||
use_socket=True,
|
||||
server_address="/run/vpp/api.sock",
|
||||
bootstrapapi=False,
|
||||
):
|
||||
"""Create a VPP API object.
|
||||
|
||||
@ -472,7 +485,9 @@ class VPPApiClient:
|
||||
self.server_address = server_address
|
||||
self._apifiles = apifiles
|
||||
self.stats = {}
|
||||
self.bootstrapapi = bootstrapapi
|
||||
|
||||
if not bootstrapapi:
|
||||
if self.apidir is None and hasattr(self.__class__, "apidir"):
|
||||
# Keep supporting the old style of providing apidir.
|
||||
self.apidir = self.__class__.apidir
|
||||
@ -485,12 +500,22 @@ class VPPApiClient:
|
||||
self.apifiles = []
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
# Bootstrap the API (memclnt.api bundled with VPP PAPI)
|
||||
resource_path = "/".join(("data", "memclnt.api.json"))
|
||||
file_content = pkg_resources.resource_string(__name__, resource_path)
|
||||
self.messages, self.services = VPPApiJSONFiles.process_json_str(
|
||||
file_content
|
||||
)
|
||||
|
||||
# Basic sanity check
|
||||
if len(self.messages) == 0 and not testmode:
|
||||
raise VPPValueError(1, "Missing JSON message definitions")
|
||||
if not bootstrapapi:
|
||||
if not (verify_enum_hint(VppEnum.vl_api_address_family_t)):
|
||||
raise VPPRuntimeError("Invalid address family hints. " "Cannot continue.")
|
||||
raise VPPRuntimeError(
|
||||
"Invalid address family hints. " "Cannot continue."
|
||||
)
|
||||
|
||||
self.transport = VppTransport(
|
||||
self, read_timeout=read_timeout, server_address=server_address
|
||||
@ -573,6 +598,21 @@ class VPPApiClient:
|
||||
else:
|
||||
self.logger.debug("No such message type or failed CRC checksum: %s", n)
|
||||
|
||||
def get_api_definitions(self):
|
||||
"""get_api_definition. Bootstrap from the embedded memclnt.api.json file."""
|
||||
|
||||
# Bootstrap so we can call the get_api_json function
|
||||
self._register_functions(do_async=False)
|
||||
|
||||
r = self.api.get_api_json()
|
||||
if r.retval != 0:
|
||||
raise VPPApiError("Failed to load API definitions from VPP")
|
||||
|
||||
# Process JSON
|
||||
m, s = VPPApiJSONFiles.process_json_array_str(r.json)
|
||||
self.messages.update(m)
|
||||
self.services.update(s)
|
||||
|
||||
def connect_internal(self, name, msg_handler, chroot_prefix, rx_qlen, do_async):
|
||||
pfx = chroot_prefix.encode("utf-8") if chroot_prefix else None
|
||||
|
||||
@ -580,6 +620,10 @@ class VPPApiClient:
|
||||
if rv != 0:
|
||||
raise VPPIOError(2, "Connect failed")
|
||||
self.vpp_dictionary_maxid = self.transport.msg_table_max_index()
|
||||
|
||||
# Register functions
|
||||
if self.bootstrapapi:
|
||||
self.get_api_definitions()
|
||||
self._register_functions(do_async=do_async)
|
||||
|
||||
# Initialise control ping
|
||||
@ -588,6 +632,7 @@ class VPPApiClient:
|
||||
("control_ping" + "_" + crc[2:])
|
||||
)
|
||||
self.control_ping_msgdef = self.messages["control_ping"]
|
||||
|
||||
if self.async_thread:
|
||||
self.event_thread = threading.Thread(target=self.thread_msg_handler)
|
||||
self.event_thread.daemon = True
|
||||
@ -659,6 +704,7 @@ class VPPApiClient:
|
||||
)
|
||||
|
||||
(i, ci, context), size = header.unpack(msg, 0)
|
||||
|
||||
if self.id_names[i] == "rx_thread_exit":
|
||||
return
|
||||
|
||||
|
Reference in New Issue
Block a user