+ The recommended ways of use are (examples):
+
+ 1. Simple request / reply. Example with no arguments:
+
+ cmd = "show_version"
+ with PapiSocketExecutor(node) as papi_exec:
+ reply = papi_exec.add(cmd).get_reply(err_msg)
+
+ 2. Dump functions:
+
+ cmd = "sw_interface_rx_placement_dump"
+ with PapiSocketExecutor(node) as papi_exec:
+ papi_exec.add(cmd, sw_if_index=ifc["vpp_sw_index"])
+ details = papi_exec.get_details(err_msg)
+
+ 3. Multiple requests with one reply each.
+ In this example, there are three requests with arguments,
+ the second and the third ones are the same but with different arguments.
+ This example also showcases method chaining.
+
+ with PapiSocketExecutor(node, is_async=True) as papi_exec:
+ replies = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
+ add(cmd2, **args3).get_replies(err_msg)
+
+ The "is_async=True" part in the last example enables "async handling mode",
+ which imposes limitations but gains speed and saves memory.
+ This is different than async mode of VPP PAPI, as the default handling mode
+ also uses async PAPI connections.
+
+ The implementation contains more hidden details, such as
+ support for old VPP PAPI async mode behavior, API CRC checking
+ conditional usage of control ping, and possible susceptibility to VPP-2033.
+ See docstring of methods for more detailed info.
+ """
+
+ # Class cache for reuse between instances.
+ api_root_dir = None
+ """We copy .api json files and PAPI code from DUT to robot machine.
+ This class variable holds temporary directory once created.
+ When python exits, the directory is deleted, so no downloaded file leaks.
+ The value will be set to TemporaryDirectory class instance (not string path)
+ to ensure deletion at exit."""
+ api_json_path = None
+ """String path to .api.json files, a directory somewhere in api_root_dir."""
+ api_package_path = None
+ """String path to PAPI code, a different directory under api_root_dir."""
+ crc_checker = None
+ """Accesses .api.json files at creation, caching speeds up accessing it."""
+ reusable_vpp_client_list = list()
+ """Each connection needs a separate client instance,
+ and each client instance creation needs to parse all .api files,
+ which takes time. If a client instance disconnects, it is put here,
+ so on next connect we can reuse intead of creating new."""
+ conn_cache = dict()
+ """Mapping from node key to connected client instance."""
+
+ def __init__(
+ self, node, remote_vpp_socket=Constants.SOCKSVR_PATH, is_async=False
+ ):
+ """Store the given arguments, declare managed variables.
+
+ :param node: Node to connect to and forward unix domain socket from.
+ :param remote_vpp_socket: Path to remote socket to tunnel to.
+ :param is_async: Whether to use async handling.
+ :type node: dict
+ :type remote_vpp_socket: str
+ :type is_async: bool
+ """
+ self._node = node
+ self._remote_vpp_socket = remote_vpp_socket
+ self._is_async = is_async
+ # The list of PAPI commands to be executed on the node.
+ self._api_command_list = list()
+
+ def ensure_api_dirs(self):
+ """Copy files from DUT to local temporary directory.
+
+ If the directory is still there, do not copy again.
+ If copying, also initialize CRC checker (this also performs
+ static checks), and remember PAPI package path.
+ Do not add that to PATH yet.
+ """
+ cls = self.__class__
+ if cls.api_package_path:
+ return
+ # Pylint suggests to use "with" statement, which we cannot,
+ # do as the dir should stay for multiple ensure_vpp_instance calls.
+ cls.api_root_dir = tempfile.TemporaryDirectory(dir="/tmp")
+ root_path = cls.api_root_dir.name
+ # Pack, copy and unpack Python part of VPP installation from _node.
+ # TODO: Use rsync or recursive version of ssh.scp_node instead?
+ node = self._node
+ exec_cmd_no_error(node, ["rm", "-rf", "/tmp/papi.txz"])
+ # Papi python version depends on OS (and time).
+ # Python 3.4 or higher, site-packages or dist-packages.
+ installed_papi_glob = "/usr/lib/python3*/*-packages/vpp_papi"
+ # We need to wrap this command in bash, in order to expand globs,
+ # and as ssh does join, the inner command has to be quoted.
+ inner_cmd = " ".join(
+ [
+ "tar",
+ "cJf",
+ "/tmp/papi.txz",
+ "--exclude=*.pyc",
+ installed_papi_glob,
+ "/usr/share/vpp/api",
+ ]
+ )
+ exec_cmd_no_error(node, ["bash", "-c", f"'{inner_cmd}'"])
+ scp_node(node, root_path + "/papi.txz", "/tmp/papi.txz", get=True)
+ run(["tar", "xf", root_path + "/papi.txz", "-C", root_path])
+ cls.api_json_path = root_path + "/usr/share/vpp/api"
+ # Perform initial checks before .api.json files are gone,
+ # by creating the checker instance.
+ cls.crc_checker = VppApiCrcChecker(cls.api_json_path)
+ # When present locally, we finally can find the installation path.
+ cls.api_package_path = glob.glob(root_path + installed_papi_glob)[0]
+ # Package path has to be one level above the vpp_papi directory.
+ cls.api_package_path = cls.api_package_path.rsplit("/", 1)[0]
+
+ def ensure_vpp_instance(self):
+ """Create or reuse a closed client instance, return it.
+
+ The instance is initialized for unix domain socket access,
+ it has initialized all the bindings, it is removed from the internal
+ list of disconnected instances, but it is not connected
+ (to a local socket) yet.
+
+ :returns: VPP client instance ready for connect.
+ :rtype: vpp_papi.VPPApiClient
+ """
+ self.ensure_api_dirs()
+ cls = self.__class__
+ if cls.reusable_vpp_client_list:
+ # Reuse in LIFO fashion.
+ *cls.reusable_vpp_client_list, ret = cls.reusable_vpp_client_list
+ return ret
+ # Creating an instance leads to dynamic imports from VPP PAPI code,
+ # so the package directory has to be present until the instance.
+ # But it is simpler to keep the package dir around.
+ try:
+ sys.path.append(cls.api_package_path)
+ # TODO: Pylint says import-outside-toplevel and import-error.
+ # It is right, we should refactor the code and move initialization
+ # of package outside.
+ from vpp_papi.vpp_papi import VPPApiClient as vpp_class
+
+ vpp_class.apidir = cls.api_json_path
+ # We need to create instance before removing from sys.path.
+ # Cannot use loglevel parameter, robot.api.logger lacks the support.
+ vpp_instance = vpp_class(
+ use_socket=True,
+ server_address="TBD",
+ async_thread=False,
+ # Large read timeout was originally there for VPP-1722,
+ # it may still be helping against AVF device creation failures.
+ read_timeout=14,
+ logger=FilteredLogger(logger, "INFO"),
+ )
+ # The following is needed to prevent union (e.g. Ip4) debug logging
+ # of VPP part of PAPI from spamming robot logs.
+ logging.getLogger("vpp_papi.serializer").setLevel(logging.INFO)
+ finally:
+ if sys.path[-1] == cls.api_package_path:
+ sys.path.pop()
+ return vpp_instance
+
+ @classmethod
+ def key_for_node_and_socket(cls, node, remote_socket):
+ """Return a hashable object to distinguish nodes.
+
+ The usual node object (of "dict" type) is not hashable,
+ and can contain mutable information (mostly virtual interfaces).
+ Use this method to get an object suitable for being a key in dict.
+
+ The fields to include are chosen by what ssh needs.
+
+ This class method is needed, for disconnect.
+
+ :param node: The node object to distinguish.
+ :param remote_socket: Path to remote socket.
+ :type node: dict
+ :type remote_socket: str
+ :return: Tuple of values distinguishing this node from similar ones.
+ :rtype: tuple of str