+ Optionally, log the field value.
+
+ :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 = u"cli_inband"
+ args = dict(
+ cmd=cli_cmd
+ )
+ err_msg = f"Failed to run 'cli_inband {cli_cmd}' PAPI command " \
+ f"on host {node[u'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"{cmd} ({node[u'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.
+
+ :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.
+
+ :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)}")
+
+ def _execute(self, err_msg=u"Undefined error message", exp_rv=0):
+ """Turn internal command list into data and execute; return replies.
+
+ This method also clears the internal command list.
+
+ IMPORTANT!
+ Do not use this method in L1 keywords. Use:
+ - get_replies()
+ - get_reply()
+ - get_sw_if_index()
+ - get_details()
+
+ :param err_msg: The message used if the PAPI command(s) execution fails.
+ :type err_msg: str
+ :returns: Papi responses parsed into a dict-like object,
+ with fields due to API (possibly including retval).
+ :rtype: list of dict
+ :raises RuntimeError: If the replies are not all correct.
+ """
+ vpp_instance = self.vpp_instance
+ local_list = self._api_command_list
+ # Clear first as execution may fail.
+ self._api_command_list = list()
+ replies = list()
+ for command in local_list:
+ api_name = command[u"api_name"]
+ papi_fn = getattr(vpp_instance.api, api_name)
+ try:
+ try:
+ reply = papi_fn(**command[u"api_args"])
+ except (IOError, struct.error) as err:
+ # Occasionally an error happens, try reconnect.
+ logger.warn(f"Reconnect after error: {err!r}")
+ self.vpp_instance.disconnect()
+ # Testing shows immediate reconnect fails.
+ time.sleep(1)
+ self.vpp_instance.connect_sync(u"csit_socket")
+ logger.trace(u"Reconnected.")
+ reply = papi_fn(**command[u"api_args"])
+ except (AttributeError, IOError, struct.error) as err:
+ raise AssertionError(err_msg) from err
+ # *_dump commands return list of objects, convert, ordinary reply.
+ if not isinstance(reply, list):
+ reply = [reply]
+ for item in reply:
+ self.crc_checker.check_api_name(item.__class__.__name__)
+ dict_item = dictize(item)
+ if u"retval" in dict_item.keys():
+ # *_details messages do not contain retval.
+ retval = dict_item[u"retval"]
+ if retval != exp_rv:
+ # TODO: What exactly to log and raise here?
+ raise AssertionError(
+ f"Retval {retval!r} does not match expected "
+ f"retval {exp_rv!r}"
+ )
+ replies.append(dict_item)
+ return replies
+
+
+class PapiExecutor:
+ """Contains methods for executing VPP Python API commands on DUTs.
+
+ TODO: Remove .add step, make get_stats accept paths directly.
+
+ This class processes only one type of VPP PAPI methods: vpp-stats.
+
+ The recommended ways of use are (examples):
+
+ path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
+ with PapiExecutor(node) as papi_exec:
+ stats = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
+
+ print('RX interface core 0, sw_if_index 0:\n{0}'.\
+ format(stats[0]['/if/rx'][0][0]))
+
+ or
+
+ path_1 = ['^/if', ]
+ path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
+ with PapiExecutor(node) as papi_exec:
+ stats = papi_exec.add('vpp-stats', path=path_1).\
+ add('vpp-stats', path=path_2).get_stats()
+
+ print('RX interface core 0, sw_if_index 0:\n{0}'.\
+ format(stats[1]['/if/rx'][0][0]))
+
+ Note: In this case, when PapiExecutor method 'add' is used:
+ - its parameter 'csit_papi_command' is used only to keep information
+ that vpp-stats are requested. It is not further processed but it is
+ included in the PAPI history this way:
+ vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
+ Always use csit_papi_command="vpp-stats" if the VPP PAPI method
+ is "stats".
+ - the second parameter must be 'path' as it is used by PapiExecutor
+ method 'add'.
+ """
+
+ def __init__(self, node):
+ """Initialization.
+
+ :param node: Node to run command(s) on.
+ :type node: dict
+ """
+ # Node to run command(s) on.
+ self._node = node
+
+ # The list of PAPI commands to be executed on the node.
+ self._api_command_list = list()
+
+ self._ssh = SSH()
+
+ def __enter__(self):
+ try:
+ self._ssh.connect(self._node)
+ except IOError:
+ raise RuntimeError(
+ f"Cannot open SSH connection to host {self._node[u'host']} "
+ f"to execute PAPI command(s)"
+ )
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self._ssh.disconnect(self._node)
+
+ def add(self, csit_papi_command=u"vpp-stats", history=True, **kwargs):
+ """Add next command to internal command list; return self.
+
+ The argument name 'csit_papi_command' must be unique enough as it cannot
+ be repeated in kwargs.
+ The kwargs dict is deep-copied, so it is safe to use the original
+ with partial modifications for subsequent commands.
+
+ :param csit_papi_command: VPP API command.
+ :param history: Enable/disable adding command to PAPI command history.
+ :param kwargs: Optional key-value arguments.
+ :type csit_papi_command: str
+ :type history: bool
+ :type kwargs: dict
+ :returns: self, so that method chaining is possible.
+ :rtype: PapiExecutor
+ """
+ if history:
+ PapiHistory.add_to_papi_history(
+ self._node, csit_papi_command, **kwargs
+ )
+ self._api_command_list.append(
+ dict(
+ api_name=csit_papi_command, api_args=copy.deepcopy(kwargs)
+ )
+ )
+ return self
+
+ def get_stats(
+ self, err_msg=u"Failed to get statistics.", timeout=120,
+ socket=Constants.SOCKSTAT_PATH):
+ """Get VPP Stats from VPP Python API.