1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Python API executor library.
20 from pprint import pformat
21 from robot.api import logger
23 from resources.libraries.python.Constants import Constants
24 from resources.libraries.python.PapiHistory import PapiHistory
25 from resources.libraries.python.PythonThree import raise_from
26 from resources.libraries.python.ssh import SSH, SSHTimeout
29 __all__ = ["PapiExecutor"]
32 class PapiExecutor(object):
33 """Contains methods for executing VPP Python API commands on DUTs.
35 Note: Use only with "with" statement, e.g.:
37 with PapiExecutor(node) as papi_exec:
38 replies = papi_exec.add('show_version').get_replies(err_msg)
40 This class processes three classes of VPP PAPI methods:
41 1. simple request / reply: method='request',
42 2. dump functions: method='dump',
43 3. vpp-stats: method='stats'.
45 The recommended ways of use are (examples):
47 1. Simple request / reply
49 a. One request with no arguments:
51 with PapiExecutor(node) as papi_exec:
52 reply = papi_exec.add('show_version').get_reply()
54 b. Three requests with arguments, the second and the third ones are the same
55 but with different arguments.
57 with PapiExecutor(node) as papi_exec:
58 replies = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
59 add(cmd2, **args3).get_replies(err_msg)
63 cmd = 'sw_interface_rx_placement_dump'
64 with PapiExecutor(node) as papi_exec:
65 details = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
70 path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
72 with PapiExecutor(node) as papi_exec:
73 stats = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
75 print('RX interface core 0, sw_if_index 0:\n{0}'.\
76 format(stats[0]['/if/rx'][0][0]))
81 path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
83 with PapiExecutor(node) as papi_exec:
84 stats = papi_exec.add('vpp-stats', path=path_1).\
85 add('vpp-stats', path=path_2).get_stats()
87 print('RX interface core 0, sw_if_index 0:\n{0}'.\
88 format(stats[1]['/if/rx'][0][0]))
90 Note: In this case, when PapiExecutor method 'add' is used:
91 - its parameter 'csit_papi_command' is used only to keep information
92 that vpp-stats are requested. It is not further processed but it is
93 included in the PAPI history this way:
94 vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
95 Always use csit_papi_command="vpp-stats" if the VPP PAPI method
97 - the second parameter must be 'path' as it is used by PapiExecutor
101 def __init__(self, node):
104 :param node: Node to run command(s) on.
108 # Node to run command(s) on.
111 # The list of PAPI commands to be executed on the node.
112 self._api_command_list = list()
118 self._ssh.connect(self._node)
120 raise RuntimeError("Cannot open SSH connection to host {host} to "
121 "execute PAPI command(s)".
122 format(host=self._node["host"]))
125 def __exit__(self, exc_type, exc_val, exc_tb):
126 self._ssh.disconnect(self._node)
128 def add(self, csit_papi_command="vpp-stats", history=True, **kwargs):
129 """Add next command to internal command list; return self.
131 The argument name 'csit_papi_command' must be unique enough as it cannot
132 be repeated in kwargs.
134 :param csit_papi_command: VPP API command.
135 :param history: Enable/disable adding command to PAPI command history.
136 :param kwargs: Optional key-value arguments.
137 :type csit_papi_command: str
140 :returns: self, so that method chaining is possible.
144 PapiHistory.add_to_papi_history(
145 self._node, csit_papi_command, **kwargs)
146 self._api_command_list.append(dict(api_name=csit_papi_command,
150 def get_stats(self, err_msg="Failed to get statistics.", timeout=120):
151 """Get VPP Stats from VPP Python API.
153 :param err_msg: The message used if the PAPI command(s) execution fails.
154 :param timeout: Timeout in seconds.
157 :returns: Requested VPP statistics.
161 paths = [cmd['api_args']['path'] for cmd in self._api_command_list]
162 self._api_command_list = list()
164 stdout = self._execute_papi(
165 paths, method='stats', err_msg=err_msg, timeout=timeout)
167 return json.loads(stdout)
169 def get_replies(self, err_msg="Failed to get replies.", timeout=120):
170 """Get replies from VPP Python API.
172 The replies are parsed into dict-like objects,
173 "retval" field is guaranteed to be zero on success.
175 :param err_msg: The message used if the PAPI command(s) execution fails.
176 :param timeout: Timeout in seconds.
179 :returns: Responses, dict objects with fields due to API and "retval".
181 :raises RuntimeError: If retval is nonzero, parsing or ssh error.
183 return self._execute(method='request', err_msg=err_msg, timeout=timeout)
185 def get_reply(self, err_msg="Failed to get reply.", timeout=120):
186 """Get reply from VPP Python API.
188 The reply is parsed into dict-like object,
189 "retval" field is guaranteed to be zero on success.
191 TODO: Discuss exception types to raise, unify with inner methods.
193 :param err_msg: The message used if the PAPI command(s) execution fails.
194 :param timeout: Timeout in seconds.
197 :returns: Response, dict object with fields due to API and "retval".
199 :raises AssertionError: If retval is nonzero, parsing or ssh error.
201 replies = self.get_replies(err_msg=err_msg, timeout=timeout)
202 if len(replies) != 1:
203 raise RuntimeError("Expected single reply, got {replies!r}".format(
207 def get_sw_if_index(self, err_msg="Failed to get reply.", timeout=120):
208 """Get sw_if_index from reply from VPP Python API.
210 Frequently, the caller is only interested in sw_if_index field
211 of the reply, this wrapper makes such call sites shorter.
213 TODO: Discuss exception types to raise, unify with inner methods.
215 :param err_msg: The message used if the PAPI command(s) execution fails.
216 :param timeout: Timeout in seconds.
219 :returns: Response, sw_if_index value of the reply.
221 :raises AssertionError: If retval is nonzero, parsing or ssh error.
223 return self.get_reply(err_msg=err_msg, timeout=timeout)["sw_if_index"]
225 def get_details(self, err_msg="Failed to get dump details.", timeout=120):
226 """Get dump details from VPP Python API.
228 The details are parsed into dict-like objects.
229 The number of details per single dump command can vary,
230 and all association between details and dumps is lost,
231 so if you care about the association (as opposed to
232 logging everything at once for debugging purposes),
233 it is recommended to call get_details for each dump (type) separately.
235 :param err_msg: The message used if the PAPI command(s) execution fails.
236 :param timeout: Timeout in seconds.
239 :returns: Details, dict objects with fields due to API without "retval".
242 return self._execute(method='dump', err_msg=err_msg, timeout=timeout)
245 def dump_and_log(node, cmds):
246 """Dump and log requested information, return None.
248 :param node: DUT node.
249 :param cmds: Dump commands to be executed.
251 :type cmds: list of str
253 with PapiExecutor(node) as papi_exec:
255 details = papi_exec.add(cmd).get_details()
256 logger.debug("{cmd}:\n{details}".format(
257 cmd=cmd, details=pformat(details)))
260 def run_cli_cmd(node, cmd, log=True):
261 """Run a CLI command as cli_inband, return the "reply" field of reply.
263 Optionally, log the field value.
265 :param node: Node to run command on.
266 :param cmd: The CLI command to be run on the node.
267 :param log: If True, the response is logged.
271 :returns: CLI output.
277 err_msg = "Failed to run 'cli_inband {cmd}' PAPI command on host " \
278 "{host}".format(host=node['host'], cmd=cmd)
280 with PapiExecutor(node) as papi_exec:
281 reply = papi_exec.add(cli, **args).get_reply(err_msg)["reply"]
284 logger.info("{cmd}:\n{reply}".format(cmd=cmd, reply=reply))
289 def _process_api_data(api_d):
290 """Process API data for smooth converting to JSON string.
292 Apply binascii.hexlify() method for string values.
294 :param api_d: List of APIs with their arguments.
296 :returns: List of APIs with arguments pre-processed for JSON.
300 def process_value(val):
303 :param val: Value to be processed.
305 :returns: Processed value.
306 :rtype: dict or str or int
308 if isinstance(val, dict):
309 for val_k, val_v in val.iteritems():
310 val[str(val_k)] = process_value(val_v)
312 elif isinstance(val, list):
313 for idx, val_l in enumerate(val):
314 val[idx] = process_value(val_l)
317 return binascii.hexlify(val) if isinstance(val, str) else val
319 api_data_processed = list()
321 api_args_processed = dict()
322 for a_k, a_v in api["api_args"].iteritems():
323 api_args_processed[str(a_k)] = process_value(a_v)
324 api_data_processed.append(dict(api_name=api["api_name"],
325 api_args=api_args_processed))
326 return api_data_processed
329 def _revert_api_reply(api_r):
330 """Process API reply / a part of API reply.
332 Apply binascii.unhexlify() method for unicode values.
334 TODO: Implement complex solution to process of replies.
336 :param api_r: API reply.
338 :returns: Processed API reply / a part of API reply.
341 def process_value(val):
344 :param val: Value to be processed.
346 :returns: Processed value.
347 :rtype: dict or str or int
349 if isinstance(val, dict):
350 for val_k, val_v in val.iteritems():
351 val[str(val_k)] = process_value(val_v)
353 elif isinstance(val, list):
354 for idx, val_l in enumerate(val):
355 val[idx] = process_value(val_l)
357 elif isinstance(val, unicode):
358 return binascii.unhexlify(val)
364 for reply_key, reply_v in api_r.iteritems():
365 for a_k, a_v in reply_v.iteritems():
366 reply_value[a_k] = process_value(a_v)
367 reply_dict[reply_key] = reply_value
370 def _process_reply(self, api_reply):
371 """Process API reply.
373 :param api_reply: API reply.
374 :type api_reply: dict or list of dict
375 :returns: Processed API reply.
378 if isinstance(api_reply, list):
379 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
381 reverted_reply = self._revert_api_reply(api_reply)
382 return reverted_reply
384 def _execute_papi(self, api_data, method='request', err_msg="",
386 """Execute PAPI command(s) on remote node and store the result.
388 :param api_data: List of APIs with their arguments.
389 :param method: VPP Python API method. Supported methods are: 'request',
391 :param err_msg: The message used if the PAPI command(s) execution fails.
392 :param timeout: Timeout in seconds.
397 :returns: Stdout from remote python utility, to be parsed by caller.
399 :raises SSHTimeout: If PAPI command(s) execution has timed out.
400 :raises RuntimeError: If PAPI executor failed due to another reason.
401 :raises AssertionError: If PAPI command(s) execution has failed.
405 raise RuntimeError("No API data provided.")
407 json_data = json.dumps(api_data) \
408 if method in ("stats", "stats_request") \
409 else json.dumps(self._process_api_data(api_data))
411 cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\
413 fw_dir=Constants.REMOTE_FW_DIR, method=method, json=json_data,
414 papi_provider=Constants.RESOURCES_PAPI_PROVIDER)
416 ret_code, stdout, _ = self._ssh.exec_command_sudo(
417 cmd=cmd, timeout=timeout, log_stdout_err=False)
418 # TODO: Fail on non-empty stderr?
420 logger.error("PAPI command(s) execution timeout on host {host}:"
421 "\n{apis}".format(host=self._node["host"],
424 except Exception as exc:
425 raise_from(RuntimeError(
426 "PAPI command(s) execution on host {host} "
427 "failed: {apis}".format(
428 host=self._node["host"], apis=api_data)), exc)
430 raise AssertionError(err_msg)
434 def _execute(self, method='request', err_msg="", timeout=120):
435 """Turn internal command list into data and execute; return replies.
437 This method also clears the internal command list.
440 Do not use this method in L1 keywords. Use:
445 :param method: VPP Python API method. Supported methods are: 'request',
447 :param err_msg: The message used if the PAPI command(s) execution fails.
448 :param timeout: Timeout in seconds.
452 :returns: Papi responses parsed into a dict-like object,
453 with field due to API or stats hierarchy.
455 :raises KeyError: If the reply is not correct.
458 local_list = self._api_command_list
460 # Clear first as execution may fail.
461 self._api_command_list = list()
463 stdout = self._execute_papi(
464 local_list, method=method, err_msg=err_msg, timeout=timeout)
467 json_data = json.loads(stdout)
468 except ValueError as err:
469 raise_from(RuntimeError(err_msg), err)
470 for data in json_data:
471 if method == "request":
472 api_reply = self._process_reply(data["api_reply"])
473 # api_reply contains single key, *_reply.
474 obj = api_reply.values()[0]
475 retval = obj["retval"]
477 # TODO: What exactly to log and raise here?
478 err = AssertionError("Got retval {rv!r}".format(rv=retval))
479 raise_from(AssertionError(err_msg), err, level="INFO")
481 elif method == "dump":
482 api_reply = self._process_reply(data["api_reply"])
483 # api_reply is a list where item contas single key, *_details.
484 for item in api_reply:
485 obj = item.values()[0]
488 # TODO: Implement support for stats.
489 raise RuntimeError("Unsuported method {method}".format(
492 # TODO: Make logging optional?
493 logger.debug("PAPI replies: {replies}".format(replies=replies))