+ @staticmethod
+ def run_cli_cmd(
+ node, cli_cmd, log=True, remote_vpp_socket=Constants.SOCKSVR_PATH
+ ):
+ """Run a CLI command as cli_inband, return the "reply" field of reply.
+
+ Optionally, log the field value.
+ This is a convenience wrapper around get_reply.
+
+ :param node: Node to run command on.
+ :param cli_cmd: The CLI command to be run on the node.
+ :param remote_vpp_socket: Path to remote socket to tunnel to.
+ :param log: If True, the response is logged.
+ :type node: dict
+ :type remote_vpp_socket: str
+ :type cli_cmd: str
+ :type log: bool
+ :returns: CLI output.
+ :rtype: str
+ """
+ cmd = "cli_inband"
+ args = dict(cmd=cli_cmd)
+ err_msg = (
+ f"Failed to run 'cli_inband {cli_cmd}' PAPI command"
+ f" on host {node['host']}"
+ )
+
+ with PapiSocketExecutor(node, remote_vpp_socket) as papi_exec:
+ reply = papi_exec.add(cmd, **args).get_reply(err_msg)["reply"]
+ if log:
+ logger.info(
+ f"{cli_cmd} ({node['host']} - {remote_vpp_socket}):\n"
+ f"{reply.strip()}"
+ )
+ return reply
+
+ @staticmethod
+ def run_cli_cmd_on_all_sockets(node, cli_cmd, log=True):
+ """Run a CLI command as cli_inband, on all sockets in topology file.
+
+ Just a run_cli_cmd, looping over sockets.
+
+ :param node: Node to run command on.
+ :param cli_cmd: The CLI command to be run on the node.
+ :param log: If True, the response is logged.
+ :type node: dict
+ :type cli_cmd: str
+ :type log: bool
+ """
+ sockets = Topology.get_node_sockets(node, socket_type=SocketType.PAPI)
+ if sockets:
+ for socket in sockets.values():
+ PapiSocketExecutor.run_cli_cmd(
+ node, cli_cmd, log=log, remote_vpp_socket=socket
+ )
+
+ @staticmethod
+ def dump_and_log(node, cmds):
+ """Dump and log requested information, return None.
+
+ Just a get_details (with logging), looping over commands.
+
+ :param node: DUT node.
+ :param cmds: Dump commands to be executed.
+ :type node: dict
+ :type cmds: list of str
+ """
+ with PapiSocketExecutor(node) as papi_exec:
+ for cmd in cmds:
+ dump = papi_exec.add(cmd).get_details()
+ logger.debug(f"{cmd}:\n{pformat(dump)}")
+
+ @staticmethod
+ def _read_internal(vpp_instance, timeout=None):
+ """Blockingly read within timeout.
+
+ This covers behaviors both before and after 37758.
+ One read attempt is guaranteed even with zero timeout.
+
+ TODO: Simplify after 2302 RCA is done.
+
+ :param vpp_instance: Client instance to read from.
+ :param timeout: How long to wait for reply (or transport default).
+ :type vpp_instance: vpp_papi.VPPApiClient
+ :type timeout: Optional[float]
+ :returns: Message read or None if nothing got read.
+ :rtype: Optional[namedtuple]
+ """
+ timeout = vpp_instance.read_timeout if timeout is None else timeout
+ if vpp_instance.csit_deque is None:
+ return vpp_instance.read_blocking(timeout=timeout)
+ time_stop = time.monotonic() + timeout
+ while 1:
+ try:
+ return vpp_instance.csit_deque.popleft()
+ except IndexError:
+ # We could busy-wait but that seems to starve the reader thread.
+ time.sleep(0.01)
+ if time.monotonic() > time_stop:
+ return None
+
+ @staticmethod
+ def _read(vpp_instance, tries=3):
+ """Blockingly read within timeout, retry on early None.
+
+ For (sometimes) unknown reasons, VPP client in async mode likes
+ to return None occasionally before time runs out.
+ This function retries in that case.
+
+ Most of the time, early None means VPP crashed (see VPP-2033),
+ but is is better to give VPP more chances to respond without failure.
+
+ TODO: Perhaps CSIT now never triggers VPP-2033,
+ so investigate and remove this layer if even more speed is needed.
+
+ :param vpp_instance: Client instance to read from.
+ :param tries: Maximum number of tries to attempt.
+ :type vpp_instance: vpp_papi.VPPApiClient
+ :type tries: int
+ :returns: Message read or None if nothing got read even with retries.
+ :rtype: Optional[namedtuple]
+ """
+ timeout = vpp_instance.read_timeout
+ for _ in range(tries):
+ time_stop = time.monotonic() + 0.9 * timeout
+ reply = PapiSocketExecutor._read_internal(vpp_instance)
+ if reply is None and time.monotonic() < time_stop:
+ logger.trace("Early None. Retry?")
+ continue
+ return reply
+ logger.trace(f"Got {tries} early Nones, probably a real None.")
+ return None
+
+ @staticmethod
+ def _drain(vpp_instance, err_msg, timeout=30.0):
+ """Keep reading with until None or timeout.
+
+ This is needed to mitigate the risk of a state with unread responses
+ (e.g. after non-zero retval in the middle of get_replies)
+ causing failures in everything subsequent (until disconnect).
+
+ The reads are done without any waiting.
+
+ It is possible some responses have not arrived yet,
+ but that is unlikely as Python is usually slower than VPP.
+
+ :param vpp_instance: Client instance to read from.
+ :param err_msg: Error message to use when overstepping timeout.
+ :param timeout: How long to try before giving up.
+ :type vpp_instance: vpp_papi.VPPApiClient
+ :type err_msg: str
+ :type timeout: float
+ :raises RuntimeError: If read keeps returning nonzero after timeout.
+ """
+ time_stop = time.monotonic() + timeout
+ while time.monotonic() < time_stop:
+ if PapiSocketExecutor._read_internal(vpp_instance, 0.0) is None:
+ return
+ raise RuntimeError(f"{err_msg}\nTimed out while draining.")
+
+ def _execute(self, err_msg, do_async, single_reply=True):
+ """Turn internal command list into data and execute; return replies.
+
+ This method also clears the internal command list.
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :param do_async: If true, assume one reply per command and do not wait
+ for each reply before sending next request.
+ Dump commands (and calls causing VPP-2033) need False.
+ :param single_reply: For sync emulation mode (cannot be False
+ if do_async is True). When false use control ping.
+ When true, wait for a single reply.
+ :type err_msg: str
+ :type do_async: bool
+ :type single_reply: bool
+ :returns: Papi replies parsed into a dict-like object,
+ with fields due to API (possibly including retval).
+ :rtype: NoneType or list of dict
+ :raises RuntimeError: If the replies are not all correct.
+ """
+ local_list = self._api_command_list
+ # Clear first as execution may fail.
+ self._api_command_list = list()
+ if do_async:
+ if not single_reply:
+ raise RuntimeError("Async papi needs one reply per request.")
+ return self._execute_async(local_list, err_msg=err_msg)
+ return self._execute_sync(
+ local_list, err_msg=err_msg, single_reply=single_reply
+ )
+
+ def _execute_sync(self, local_list, err_msg, single_reply):
+ """Execute commands waiting for replies one by one; return replies.
+
+ This implementation either expects a single response per request,
+ or uses control ping to emulate sync PAPI calls.
+ Reliable, but slow. Required for dumps. Needed for calls
+ which trigger VPP-2033.
+
+ CRC checking is done for the replies (requests are checked in .add).
+
+ :param local_list: The list of PAPI commands to be executed on the node.
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :param single_reply: When false use control ping.
+ When true, wait for a single reply.
+ :type local_list: list of dict
+ :type err_msg: str
+ :type single_reply: bool
+ :returns: Papi replies parsed into a dict-like object,
+ with fields due to API (possibly including retval).
+ :rtype: List[UserDict]
+ :raises AttributeError: If VPP does not know the command.
+ :raises RuntimeError: If the replies are not all correct.