diff options
author | Peter Mikus <pmikus@cisco.com> | 2019-08-30 13:20:14 +0000 |
---|---|---|
committer | Peter Mikus <pmikus@cisco.com> | 2019-09-10 07:28:44 +0000 |
commit | c4528bcd137c0813d34b1b248bc92670736e78e6 (patch) | |
tree | ca5ea6a5b6846dfbc0c7c1937dd1e6a6e469a985 /resources/libraries/python | |
parent | 7232ab0e49554a653f3527df5a2ba055f804a4fb (diff) |
Refactor getting telemetry
+ Ability to get stats from CNF via SocketPAPI
- Remove obsolete functions
Signed-off-by: Peter Mikus <pmikus@cisco.com>
Change-Id: I4d1b32a7279244592be96644e4f8a530c4f29a15
Diffstat (limited to 'resources/libraries/python')
-rw-r--r-- | resources/libraries/python/Constants.py | 3 | ||||
-rw-r--r-- | resources/libraries/python/ContainerUtils.py | 24 | ||||
-rw-r--r-- | resources/libraries/python/PapiExecutor.py | 52 | ||||
-rw-r--r-- | resources/libraries/python/VppCounters.py | 283 | ||||
-rw-r--r-- | resources/libraries/python/topology.py | 72 |
5 files changed, 230 insertions, 204 deletions
diff --git a/resources/libraries/python/Constants.py b/resources/libraries/python/Constants.py index 9606a10fad..b7f91930cb 100644 --- a/resources/libraries/python/Constants.py +++ b/resources/libraries/python/Constants.py @@ -228,6 +228,9 @@ class Constants(object): # /tmp directory is inside the DUT1 docker. DUT1_UUID = get_str_from_env("DUT1_UUID", "") + # Default path to VPP API Stats socket. + SOCKSTAT_PATH = "/run/vpp/stats.sock" + # Global "kill switch" for CRC checking during runtime. FAIL_ON_CRC_MISMATCH = get_optimistic_bool_from_env("FAIL_ON_CRC_MISMATCH") diff --git a/resources/libraries/python/ContainerUtils.py b/resources/libraries/python/ContainerUtils.py index 363411c070..cd48fc6c1f 100644 --- a/resources/libraries/python/ContainerUtils.py +++ b/resources/libraries/python/ContainerUtils.py @@ -21,7 +21,7 @@ from collections import OrderedDict, Counter from resources.libraries.python.ssh import SSH from resources.libraries.python.Constants import Constants -from resources.libraries.python.topology import Topology +from resources.libraries.python.topology import Topology, SocketType from resources.libraries.python.VppConfigGenerator import VppConfigGenerator @@ -430,6 +430,22 @@ class ContainerEngine(object): self.execute('supervisorctl reload') self.execute('supervisorctl start vpp') + from robot.libraries.BuiltIn import BuiltIn + topo_instance = BuiltIn().get_library_instance( + 'resources.libraries.python.topology.Topology') + topo_instance.add_new_socket( + self.container.node, + SocketType.PAPI, + self.container.name, + '{root}/tmp/vpp_sockets/{name}/api.sock'. + format(root=self.container.root, name=self.container.name)) + topo_instance.add_new_socket( + self.container.node, + SocketType.STATS, + self.container.name, + '{root}/tmp/vpp_sockets/{name}/stats.sock'. + format(root=self.container.root, name=self.container.name)) + def restart_vpp(self): """Restart VPP service inside a container.""" self.execute('supervisorctl restart vpp') @@ -449,7 +465,8 @@ class ContainerEngine(object): vpp_config.add_unix_cli_listen() vpp_config.add_unix_nodaemon() vpp_config.add_unix_exec('/tmp/running.exec') - vpp_config.add_socksvr() + vpp_config.add_socksvr(socket=Constants.SOCKSVR_PATH) + vpp_config.add_statseg_per_node_counters(value='on') # We will pop the first core from the list to be a main core vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0))) # If more cores in the list, the rest will be used as workers. @@ -499,7 +516,8 @@ class ContainerEngine(object): vpp_config.add_unix_cli_listen() vpp_config.add_unix_nodaemon() vpp_config.add_unix_exec('/tmp/running.exec') - vpp_config.add_socksvr() + vpp_config.add_socksvr(socket=Constants.SOCKSVR_PATH) + vpp_config.add_statseg_per_node_counters(value='on') vpp_config.add_plugin('disable', 'dpdk_plugin.so') # Apply configuration diff --git a/resources/libraries/python/PapiExecutor.py b/resources/libraries/python/PapiExecutor.py index 74ff7a0f9b..aec43b6694 100644 --- a/resources/libraries/python/PapiExecutor.py +++ b/resources/libraries/python/PapiExecutor.py @@ -34,6 +34,7 @@ from resources.libraries.python.PythonThree import raise_from from resources.libraries.python.PapiHistory import PapiHistory from resources.libraries.python.ssh import ( SSH, SSHTimeout, exec_cmd_no_error, scp_node) +from resources.libraries.python.topology import Topology, SocketType from resources.libraries.python.VppApiCrc import VppApiCrcChecker @@ -397,7 +398,7 @@ class PapiSocketExecutor(object): :raises AssertionError: If retval is nonzero, parsing or ssh error. """ reply = self.get_reply(err_msg=err_msg) - logger.info("Getting index from {reply!r}".format(reply=reply)) + logger.trace("Getting index from {reply!r}".format(reply=reply)) return reply["sw_if_index"] def get_details(self, err_msg="Failed to get dump details."): @@ -418,15 +419,18 @@ class PapiSocketExecutor(object): return self._execute(err_msg) @staticmethod - def run_cli_cmd(node, cmd, log=True): + def run_cli_cmd(node, 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. :param node: Node to run command on. :param 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 cmd: str :type log: bool :returns: CLI output. @@ -436,13 +440,33 @@ class PapiSocketExecutor(object): args = dict(cmd=cmd) err_msg = "Failed to run 'cli_inband {cmd}' PAPI command on host " \ "{host}".format(host=node['host'], cmd=cmd) - with PapiSocketExecutor(node) as papi_exec: + with PapiSocketExecutor(node, remote_vpp_socket) as papi_exec: reply = papi_exec.add(cli, **args).get_reply(err_msg)["reply"] if log: - logger.info("{cmd}:\n{reply}".format(cmd=cmd, reply=reply)) + logger.info( + "{cmd} ({host} - {remote_vpp_socket}):\n{reply}". + format(cmd=cmd, reply=reply, + remote_vpp_socket=remote_vpp_socket, host=node['host'])) return reply @staticmethod + def run_cli_cmd_on_all_sockets(node, cmd, log=True): + """Run a CLI command as cli_inband, on all sockets in topology file. + + :param node: Node to run command on. + :param cmd: The CLI command to be run on the node. + :param log: If True, the response is logged. + :type node: dict + :type 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, cmd, log=log, remote_vpp_socket=socket) + + @staticmethod def dump_and_log(node, cmds): """Dump and log requested information, return None. @@ -607,7 +631,8 @@ class PapiExecutor(object): api_name=csit_papi_command, api_args=copy.deepcopy(kwargs))) return self - def get_stats(self, err_msg="Failed to get statistics.", timeout=120): + def get_stats(self, err_msg="Failed to get statistics.", timeout=120, + socket=Constants.SOCKSTAT_PATH): """Get VPP Stats from VPP Python API. :param err_msg: The message used if the PAPI command(s) execution fails. @@ -617,12 +642,12 @@ class PapiExecutor(object): :returns: Requested VPP statistics. :rtype: list of dict """ - paths = [cmd['api_args']['path'] for cmd in self._api_command_list] self._api_command_list = list() stdout = self._execute_papi( - paths, method='stats', err_msg=err_msg, timeout=timeout) + paths, method='stats', err_msg=err_msg, timeout=timeout, + socket=socket) return json.loads(stdout) @@ -667,7 +692,7 @@ class PapiExecutor(object): return api_data_processed def _execute_papi(self, api_data, method='request', err_msg="", - timeout=120): + timeout=120, socket=None): """Execute PAPI command(s) on remote node and store the result. :param api_data: List of APIs with their arguments. @@ -685,7 +710,6 @@ class PapiExecutor(object): :raises RuntimeError: If PAPI executor failed due to another reason. :raises AssertionError: If PAPI command(s) execution has failed. """ - if not api_data: raise RuntimeError("No API data provided.") @@ -693,10 +717,12 @@ class PapiExecutor(object): if method in ("stats", "stats_request") \ else json.dumps(self._process_api_data(api_data)) - cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\ - format( - fw_dir=Constants.REMOTE_FW_DIR, method=method, json=json_data, - papi_provider=Constants.RESOURCES_PAPI_PROVIDER) + sock = " --socket {socket}".format(socket=socket) if socket else "" + cmd = ( + "{fw_dir}/{papi_provider} --method {method} --data '{json}'{socket}" + .format(fw_dir=Constants.REMOTE_FW_DIR, + papi_provider=Constants.RESOURCES_PAPI_PROVIDER, + method=method, json=json_data, socket=sock)) try: ret_code, stdout, _ = self._ssh.exec_command_sudo( cmd=cmd, timeout=timeout, log_stdout_err=False) diff --git a/resources/libraries/python/VppCounters.py b/resources/libraries/python/VppCounters.py index f847ca6901..65d647709d 100644 --- a/resources/libraries/python/VppCounters.py +++ b/resources/libraries/python/VppCounters.py @@ -13,14 +13,12 @@ """VPP counters utilities library.""" -import time - from pprint import pformat from robot.api import logger from resources.libraries.python.PapiExecutor import PapiExecutor from resources.libraries.python.PapiExecutor import PapiSocketExecutor -from resources.libraries.python.topology import NodeType, Topology +from resources.libraries.python.topology import Topology, SocketType, NodeType class VppCounters(object): @@ -30,49 +28,25 @@ class VppCounters(object): self._stats_table = None @staticmethod - def _get_non_zero_items(data): - """Extract and return non-zero items from the input data. - - :param data: Data to filter. - :type data: dict - :returns: Dictionary with non-zero items. - :rtype dict - """ - return {k: data[k] for k in data.keys() if sum(data[k])} - - @staticmethod def vpp_show_errors(node): """Run "show errors" debug CLI command. :param node: Node to run command on. :type node: dict """ - PapiSocketExecutor.run_cli_cmd(node, 'show errors') - - @staticmethod - def vpp_show_errors_verbose(node): - """Run "show errors verbose" debug CLI command. - - :param node: Node to run command on. - :type node: dict - """ - PapiSocketExecutor.run_cli_cmd(node, 'show errors verbose') + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, 'show errors') @staticmethod - def vpp_show_errors_on_all_duts(nodes, verbose=False): + def vpp_show_errors_on_all_duts(nodes): """Show errors on all DUTs. :param nodes: VPP nodes. - :param verbose: If True show verbose output. :type nodes: dict - :type verbose: bool """ for node in nodes.values(): if node['type'] == NodeType.DUT: - if verbose: - VppCounters.vpp_show_errors_verbose(node) - else: - VppCounters.vpp_show_errors(node) + VppCounters.vpp_show_errors(node) @staticmethod def vpp_show_runtime(node, log_zeros=False): @@ -84,68 +58,63 @@ class VppCounters(object): :type log_zeros: bool """ args = dict(path='^/sys/node') - with PapiExecutor(node) as papi_exec: - stats = papi_exec.add("vpp-stats", **args).get_stats()[0] - # TODO: Introduce get_stat? - - names = stats['/sys/node/names'] - - if not names: - return - - runtime = [] - runtime_non_zero = [] - - for name in names: - runtime.append({'name': name}) - - for idx, runtime_item in enumerate(runtime): - - calls_th = [] - for thread in stats['/sys/node/calls']: - calls_th.append(thread[idx]) - runtime_item["calls"] = calls_th - - vectors_th = [] - for thread in stats['/sys/node/vectors']: - vectors_th.append(thread[idx]) - runtime_item["vectors"] = vectors_th - - suspends_th = [] - for thread in stats['/sys/node/suspends']: - suspends_th.append(thread[idx]) - runtime_item["suspends"] = suspends_th - - clocks_th = [] - for thread in stats['/sys/node/clocks']: - clocks_th.append(thread[idx]) - runtime_item["clocks"] = clocks_th - - if (sum(calls_th) or sum(vectors_th) or - sum(suspends_th) or sum(clocks_th)): - runtime_non_zero.append(runtime_item) - - if log_zeros: - logger.info("Runtime:\n{runtime}".format( - runtime=pformat(runtime))) - else: - logger.info("Runtime:\n{runtime}".format( - runtime=pformat(runtime_non_zero))) - - @staticmethod - def vpp_show_runtime_verbose(node): - """Run "show runtime verbose" CLI command. - - TODO: Remove? - Only verbose output is possible to get using VPPStats. - - :param node: Node to run command on. - :type node: dict - """ - VppCounters.vpp_show_runtime(node) + sockets = Topology.get_node_sockets(node, socket_type=SocketType.STATS) + if sockets: + for socket in sockets.values(): + with PapiExecutor(node) as papi_exec: + stats = papi_exec.add("vpp-stats", **args).\ + get_stats(socket=socket)[0] + + names = stats['/sys/node/names'] + + if not names: + return + + runtime = [] + runtime_non_zero = [] + + for name in names: + runtime.append({'name': name}) + + for idx, runtime_item in enumerate(runtime): + + calls_th = [] + for thread in stats['/sys/node/calls']: + calls_th.append(thread[idx]) + runtime_item["calls"] = calls_th + + vectors_th = [] + for thread in stats['/sys/node/vectors']: + vectors_th.append(thread[idx]) + runtime_item["vectors"] = vectors_th + + suspends_th = [] + for thread in stats['/sys/node/suspends']: + suspends_th.append(thread[idx]) + runtime_item["suspends"] = suspends_th + + clocks_th = [] + for thread in stats['/sys/node/clocks']: + clocks_th.append(thread[idx]) + runtime_item["clocks"] = clocks_th + + if (sum(calls_th) or sum(vectors_th) or + sum(suspends_th) or sum(clocks_th)): + runtime_non_zero.append(runtime_item) + + if log_zeros: + logger.info( + "stats runtime ({host} - {socket}):\n{runtime}".format( + host=node['host'], runtime=pformat(runtime), + socket=socket)) + else: + logger.info( + "stats runtime ({host} - {socket}):\n{runtime}".format( + host=node['host'], runtime=pformat(runtime_non_zero), + socket=socket)) @staticmethod - def show_runtime_counters_on_all_duts(nodes): + def vpp_show_runtime_counters_on_all_duts(nodes): """Clear VPP runtime counters on all DUTs. :param nodes: VPP nodes. @@ -162,7 +131,8 @@ class VppCounters(object): :param node: Node to run command on. :type node: dict """ - PapiSocketExecutor.run_cli_cmd(node, 'show hardware verbose') + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, 'show hardware verbose') @staticmethod def vpp_show_memory(node): @@ -182,13 +152,12 @@ class VppCounters(object): :param node: Node to run command on. :type node: dict - :returns: Verified data from PAPI response. - :rtype: dict """ - return PapiSocketExecutor.run_cli_cmd(node, 'clear runtime', log=False) + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, 'clear runtime', log=False) @staticmethod - def clear_runtime_counters_on_all_duts(nodes): + def vpp_clear_runtime_counters_on_all_duts(nodes): """Run "clear runtime" CLI command on all DUTs. :param nodes: VPP nodes. @@ -199,29 +168,6 @@ class VppCounters(object): VppCounters.vpp_clear_runtime(node) @staticmethod - def vpp_clear_interface_counters(node): - """Run "clear interfaces" CLI command. - - :param node: Node to run command on. - :type node: dict - :returns: Verified data from PAPI response. - :rtype: dict - """ - return PapiSocketExecutor.run_cli_cmd( - node, 'clear interfaces', log=False) - - @staticmethod - def clear_interface_counters_on_all_duts(nodes): - """Clear interface counters on all DUTs. - - :param nodes: VPP nodes. - :type nodes: dict - """ - for node in nodes.values(): - if node['type'] == NodeType.DUT: - VppCounters.vpp_clear_interface_counters(node) - - @staticmethod def vpp_clear_hardware_counters(node): """Run "clear hardware" CLI command. @@ -230,10 +176,11 @@ class VppCounters(object): :returns: Verified data from PAPI response. :rtype: dict """ - return PapiSocketExecutor.run_cli_cmd(node, 'clear hardware', log=False) + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, 'clear hardware', log=False) @staticmethod - def clear_hardware_counters_on_all_duts(nodes): + def vpp_clear_hardware_counters_on_all_duts(nodes): """Clear hardware counters on all DUTs. :param nodes: VPP nodes. @@ -249,13 +196,12 @@ class VppCounters(object): :param node: Node to run command on. :type node: dict - :returns: Verified data from PAPI response. - :rtype: dict """ - return PapiSocketExecutor.run_cli_cmd(node, 'clear errors', log=False) + PapiSocketExecutor.run_cli_cmd_on_all_sockets( + node, 'clear errors', log=False) @staticmethod - def clear_error_counters_on_all_duts(nodes): + def vpp_clear_error_counters_on_all_duts(nodes): """Clear VPP errors counters on all DUTs. :param nodes: VPP nodes. @@ -265,61 +211,6 @@ class VppCounters(object): if node['type'] == NodeType.DUT: VppCounters.vpp_clear_errors_counters(node) - def vpp_get_ipv4_interface_counter(self, node, interface): - """ - - :param node: Node to get interface IPv4 counter on. - :param interface: Interface name. - :type node: dict - :type interface: str - :returns: Interface IPv4 counter. - :rtype: int - """ - return self.vpp_get_ipv46_interface_counter(node, interface, False) - - def vpp_get_ipv6_interface_counter(self, node, interface): - """ - - :param node: Node to get interface IPv6 counter on. - :param interface: Interface name. - :type node: dict - :type interface: str - :returns: Interface IPv6 counter. - :rtype: int - """ - return self.vpp_get_ipv46_interface_counter(node, interface, True) - - def vpp_get_ipv46_interface_counter(self, node, interface, is_ipv6=True): - """Return interface IPv4/IPv6 counter. - - :param node: Node to get interface IPv4/IPv6 counter on. - :param interface: Interface name. - :param is_ipv6: Specify IP version. - :type node: dict - :type interface: str - :type is_ipv6: bool - :returns: Interface IPv4/IPv6 counter. - :rtype: int - """ - version = 'ip6' if is_ipv6 else 'ip4' - topo = Topology() - sw_if_index = topo.get_interface_sw_index(node, interface) - if sw_if_index is None: - logger.trace('{i} sw_if_index not found.'.format(i=interface)) - return 0 - - if_counters = self._stats_table.get('interface_counters') - if not if_counters: - logger.trace('No interface counters.') - return 0 - for counter in if_counters: - if counter['vnet_counter_type'] == version: - data = counter['data'] - return data[sw_if_index] - logger.trace('{i} {v} counter not found.'.format( - i=interface, v=version)) - return 0 - @staticmethod def show_vpp_statistics(node): """Show [error, hardware, interface] stats. @@ -333,16 +224,34 @@ class VppCounters(object): VppCounters.vpp_show_memory(node) @staticmethod - def show_statistics_on_all_duts(nodes, sleeptime=5): - """Show VPP statistics on all DUTs. + def show_statistics_on_all_duts(nodes): + """Show statistics on all DUTs. - :param nodes: VPP nodes. + :param nodes: DUT nodes. :type nodes: dict - :param sleeptime: Time to wait for traffic to arrive back to TG. - :type sleeptime: int """ - logger.trace('Waiting for statistics to be collected') - time.sleep(sleeptime) for node in nodes.values(): if node['type'] == NodeType.DUT: VppCounters.show_vpp_statistics(node) + + @staticmethod + def clear_vpp_statistics(node): + """Clear [error, hardware, interface] stats. + + :param node: VPP node. + :type node: dict + """ + VppCounters.vpp_clear_errors_counters(node) + VppCounters.vpp_clear_hardware_counters(node) + VppCounters.vpp_clear_runtime(node) + + @staticmethod + def clear_statistics_on_all_duts(nodes): + """Clear statistics on all DUTs. + + :param nodes: DUT nodes. + :type nodes: dict + """ + for node in nodes.values(): + if node['type'] == NodeType.DUT: + VppCounters.clear_vpp_statistics(node) diff --git a/resources/libraries/python/topology.py b/resources/libraries/python/topology.py index 1e5ce4bd62..91578a5ccf 100644 --- a/resources/libraries/python/topology.py +++ b/resources/libraries/python/topology.py @@ -23,7 +23,7 @@ from robot.api import logger from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError from robot.api.deco import keyword -__all__ = ["DICT__nodes", 'Topology', 'NodeType'] +__all__ = ["DICT__nodes", 'Topology', 'NodeType', 'SocketType'] def load_topo_from_yaml(): @@ -61,6 +61,12 @@ class NodeSubTypeTG(object): # IxNetwork IXNET = 'IXNET' +class SocketType(object): + """Defines socket types used in topology dictionaries.""" + # VPP Socket PAPI + PAPI = 'PAPI' + # VPP PAPI Stats (legacy option until stats are migrated to Socket PAPI) + STATS = 'STATS' DICT__nodes = load_topo_from_yaml() @@ -83,6 +89,26 @@ class Topology(object): the methods without having filled active topology with internal nodes data. """ + def add_node_item(self, node, value, path): + """Add item to topology node. + + :param node: Topology node. + :param value: Value to insert. + :param path: Path where to insert item. + :type node: dict + :type value: str + :type path: list + """ + if len(path) == 1: + node[path[0]] = value + return + if path[0] not in node: + node[path[0]] = {} + elif isinstance(node[path[0]], str): + node[path[0]] = {} if node[path[0]] == '' \ + else {node[path[0]]: ''} + self.add_node_item(node[path[0]], value, path[1:]) + @staticmethod def add_new_port(node, ptype): """Add new port to the node to active topology. @@ -1033,3 +1059,47 @@ class Topology(object): return iface_key except KeyError: return None + + def add_new_socket(self, node, socket_type, socket_id, socket_path): + """Add socket file of specific SocketType and ID to node. + + :param node: Node to add socket on. + :param socket_type: Socket type. + :param socket_id: Socket id. + :param socket_path: Socket absolute path. + :type node: dict + :type socket_type: SocketType + :type socket_id: str + :type socket path: str + """ + path = ['sockets', socket_type, socket_id] + self.add_node_item(node, socket_path, path) + + @staticmethod + def get_node_sockets(node, socket_type=None): + """Get node socket files. + + :param node: Node to get sockets from. + :param socket_type: Socket type or None for all sockets. + :type node: dict + :type socket_type: SocketType + :returns: Node sockets or None if not found. + :rtype: list + """ + try: + if socket_type: + return node['sockets'][socket_type] + return node['sockets'] + except KeyError: + return None + + @staticmethod + def clean_sockets_on_all_nodes(nodes): + """Remove temporary socket files from topology file. + + :param nodes: SUT nodes. + :type node: dict + """ + for node in nodes.values(): + if 'sockets' in node.keys(): + node.pop('sockets') |