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:
Ole Troan
2024-01-23 18:56:23 +01:00
committed by Andrew Yourtchenko
parent f34b6800de
commit ac0babd412
10 changed files with 968 additions and 29 deletions

View File

@ -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}"
)

View File

@ -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",

View File

@ -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(

View File

@ -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

View File

@ -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;

View File

@ -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[];
};

View File

@ -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

View File

@ -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,
)

File diff suppressed because it is too large Load Diff

View File

@ -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