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
22 from robot.api import logger
24 from resources.libraries.python.Constants import Constants
25 from resources.libraries.python.ssh import SSH, SSHTimeout
26 from resources.libraries.python.PapiHistory import PapiHistory
29 __all__ = ["PapiExecutor", "PapiResponse"]
32 class PapiResponse(object):
33 """Class for metadata specifying the Papi reply, stdout, stderr and return
37 def __init__(self, papi_reply=None, stdout="", stderr="", requests=None):
38 """Construct the Papi response by setting the values needed.
41 Implement 'dump' analogue of verify_replies that would concatenate
42 the values, so that call sites do not have to do that themselves.
44 :param papi_reply: API reply from last executed PAPI command(s).
45 :param stdout: stdout from last executed PAPI command(s).
46 :param stderr: stderr from last executed PAPI command(s).
47 :param requests: List of used PAPI requests. It is used while verifying
48 replies. If None, expected replies must be provided for verify_reply
49 and verify_replies methods.
50 :type papi_reply: list or None
56 # API reply from last executed PAPI command(s).
57 self.reply = papi_reply
59 # stdout from last executed PAPI command(s).
62 # stderr from last executed PAPI command(s).
65 # List of used PAPI requests.
66 self.requests = requests
68 # List of expected PAPI replies. It is used while verifying replies.
70 self.expected_replies = \
71 ["{rqst}_reply".format(rqst=rqst) for rqst in self.requests]
74 """Return string with human readable description of the PapiResponse.
76 :returns: Readable description.
80 "papi_reply={papi_reply},stdout={stdout},stderr={stderr},"
81 "requests={requests}").format(
82 papi_reply=self.reply, stdout=self.stdout, stderr=self.stderr,
83 requests=self.requests)
86 """Return string executable as Python constructor call.
88 :returns: Executable constructor call.
91 return "PapiResponse({str})".format(str=str(self))
93 def verify_reply(self, cmd_reply=None, idx=0,
94 err_msg="Failed to verify PAPI reply."):
95 """Verify and return data from the PAPI response.
97 Note: Use only with a simple request / reply command. In this case the
98 PAPI reply includes 'retval' which is checked in this method.
100 Do not use with 'dump' and 'vpp-stats' methods.
102 Use if PAPI response includes only one command reply.
104 Use it this way (preferred):
106 with PapiExecutor(node) as papi_exec:
107 data = papi_exec.add('show_version').get_replies().verify_reply()
109 or if you must provide the expected reply (not recommended):
111 with PapiExecutor(node) as papi_exec:
112 data = papi_exec.add('show_version').get_replies().\
113 verify_reply('show_version_reply')
115 :param cmd_reply: PAPI reply. If None, list of 'requests' should have
116 been provided to the __init__ method as pre-generated list of
117 replies is used in this method in this case.
118 The PapiExecutor._execute() method provides the requests
120 :param idx: Index to PapiResponse.reply list.
121 :param err_msg: The message used if the verification fails.
124 :type err_msg: str or None
125 :returns: Verified data from PAPI response.
127 :raises AssertionError: If the PAPI return value is not 0, so the reply
129 :raises KeyError, IndexError: If the reply does not have expected
132 cmd_rpl = self.expected_replies[idx] if cmd_reply is None else cmd_reply
134 data = self.reply[idx]['api_reply'][cmd_rpl]
135 if data['retval'] != 0:
136 raise AssertionError("{msg}\nidx={idx}, cmd_reply={reply}".
137 format(msg=err_msg, idx=idx, reply=cmd_rpl))
141 def verify_replies(self, cmd_replies=None,
142 err_msg="Failed to verify PAPI reply."):
143 """Verify and return data from the PAPI response.
145 Note: Use only with request / reply commands. In this case each
146 PAPI reply includes 'retval' which is checked.
148 Do not use with 'dump' and 'vpp-stats' methods.
150 Use if PAPI response includes more than one command reply.
154 with PapiExecutor(node) as papi_exec:
155 papi_exec.add(cmd1, **args1).add(cmd2, **args2).add(cmd2, **args3).\
156 get_replies(err_msg).verify_replies()
158 or if you need the data from the PAPI response:
160 with PapiExecutor(node) as papi_exec:
161 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
162 add(cmd2, **args3).get_replies(err_msg).verify_replies()
164 or if you must provide the list of expected replies (not recommended):
166 with PapiExecutor(node) as papi_exec:
167 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
168 add(cmd2, **args3).get_replies(err_msg).\
169 verify_replies(cmd_replies=cmd_replies)
171 :param cmd_replies: List of PAPI command replies. If None, list of
172 'requests' should have been provided to the __init__ method as
173 pre-generated list of replies is used in this method in this case.
174 The PapiExecutor._execute() method provides the requests
176 :param err_msg: The message used if the verification fails.
177 :type cmd_replies: list of str or None
179 :returns: List of verified data from PAPI response.
181 :raises AssertionError: If the PAPI response does not include at least
182 one of specified command replies.
186 cmd_rpls = self.expected_replies if cmd_replies is None else cmd_replies
188 if len(self.reply) != len(cmd_rpls):
189 raise AssertionError(err_msg)
190 for idx, cmd_reply in enumerate(cmd_rpls):
191 data.append(self.verify_reply(cmd_reply, idx, err_msg))
196 class PapiExecutor(object):
197 """Contains methods for executing VPP Python API commands on DUTs.
199 Note: Use only with "with" statement, e.g.:
201 with PapiExecutor(node) as papi_exec:
202 papi_resp = papi_exec.add('show_version').get_replies(err_msg)
204 This class processes three classes of VPP PAPI methods:
205 1. simple request / reply: method='request',
206 2. dump functions: method='dump',
207 3. vpp-stats: method='stats'.
209 The recommended ways of use are (examples):
211 1. Simple request / reply
213 a. One request with no arguments:
215 with PapiExecutor(node) as papi_exec:
216 data = papi_exec.add('show_version').get_replies().\
219 b. Three requests with arguments, the second and the third ones are the same
220 but with different arguments.
222 with PapiExecutor(node) as papi_exec:
223 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
224 add(cmd2, **args3).get_replies(err_msg).verify_replies()
228 cmd = 'sw_interface_rx_placement_dump'
229 with PapiExecutor(node) as papi_exec:
230 papi_resp = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
235 path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
237 with PapiExecutor(node) as papi_exec:
238 data = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
240 print('RX interface core 0, sw_if_index 0:\n{0}'.\
241 format(data[0]['/if/rx'][0][0]))
246 path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
248 with PapiExecutor(node) as papi_exec:
249 data = papi_exec.add('vpp-stats', path=path_1).\
250 add('vpp-stats', path=path_2).get_stats()
252 print('RX interface core 0, sw_if_index 0:\n{0}'.\
253 format(data[1]['/if/rx'][0][0]))
255 Note: In this case, when PapiExecutor method 'add' is used:
256 - its parameter 'csit_papi_command' is used only to keep information
257 that vpp-stats are requested. It is not further processed but it is
258 included in the PAPI history this way:
259 vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
260 Always use csit_papi_command="vpp-stats" if the VPP PAPI method
262 - the second parameter must be 'path' as it is used by PapiExecutor
266 def __init__(self, node):
269 :param node: Node to run command(s) on.
273 # Node to run command(s) on.
276 # The list of PAPI commands to be executed on the node.
277 self._api_command_list = list()
283 self._ssh.connect(self._node)
285 raise RuntimeError("Cannot open SSH connection to host {host} to "
286 "execute PAPI command(s)".
287 format(host=self._node["host"]))
290 def __exit__(self, exc_type, exc_val, exc_tb):
291 self._ssh.disconnect(self._node)
293 def add(self, csit_papi_command="vpp-stats", **kwargs):
294 """Add next command to internal command list; return self.
296 The argument name 'csit_papi_command' must be unique enough as it cannot
297 be repeated in kwargs.
299 :param csit_papi_command: VPP API command.
300 :param kwargs: Optional key-value arguments.
301 :type csit_papi_command: str
303 :returns: self, so that method chaining is possible.
306 PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
307 self._api_command_list.append(dict(api_name=csit_papi_command,
311 def get_stats(self, err_msg="Failed to get statistics.", timeout=120):
312 """Get VPP Stats from VPP Python API.
314 :param err_msg: The message used if the PAPI command(s) execution fails.
315 :param timeout: Timeout in seconds.
318 :returns: Requested VPP statistics.
322 paths = [cmd['api_args']['path'] for cmd in self._api_command_list]
323 self._api_command_list = list()
325 stdout, _ = self._execute_papi(
326 paths, method='stats', err_msg=err_msg, timeout=timeout)
328 return json.loads(stdout)
330 def get_stats_reply(self, err_msg="Failed to get statistics.", timeout=120):
331 """Get VPP Stats reply from VPP Python API.
333 :param err_msg: The message used if the PAPI command(s) execution fails.
334 :param timeout: Timeout in seconds.
337 :returns: Requested VPP statistics.
341 args = self._api_command_list[0]['api_args']
342 self._api_command_list = list()
344 stdout, _ = self._execute_papi(
345 args, method='stats_request', err_msg=err_msg, timeout=timeout)
347 return json.loads(stdout)
349 def get_replies(self, err_msg="Failed to get replies.",
350 process_reply=True, ignore_errors=False, timeout=120):
351 """Get reply/replies from VPP Python API.
353 :param err_msg: The message used if the PAPI command(s) execution fails.
354 :param process_reply: Process PAPI reply if True.
355 :param ignore_errors: If true, the errors in the reply are ignored.
356 :param timeout: Timeout in seconds.
358 :type process_reply: bool
359 :type ignore_errors: bool
361 :returns: Papi response including: papi reply, stdout, stderr and
365 return self._execute(
366 method='request', process_reply=process_reply,
367 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
369 def get_dump(self, err_msg="Failed to get dump.",
370 process_reply=True, ignore_errors=False, timeout=120):
371 """Get dump from VPP Python API.
373 :param err_msg: The message used if the PAPI command(s) execution fails.
374 :param process_reply: Process PAPI reply if True.
375 :param ignore_errors: If true, the errors in the reply are ignored.
376 :param timeout: Timeout in seconds.
378 :type process_reply: bool
379 :type ignore_errors: bool
381 :returns: Papi response including: papi reply, stdout, stderr and
385 return self._execute(
386 method='dump', process_reply=process_reply,
387 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
390 def dump_and_log(node, cmds):
391 """Dump and log requested information.
393 :param node: DUT node.
394 :param cmds: Dump commands to be executed.
398 with PapiExecutor(node) as papi_exec:
400 dump = papi_exec.add(cmd).get_dump()
401 logger.debug("{cmd}:\n{data}".format(
402 cmd=cmd, data=pformat(dump.reply[0]["api_reply"])))
405 def run_cli_cmd(node, cmd, log=True):
406 """Run a CLI command.
408 :param node: Node to run command on.
409 :param cmd: The CLI command to be run on the node.
410 :param log: If True, the response is logged.
414 :returns: Verified data from PAPI response.
420 err_msg = "Failed to run 'cli_inband {cmd}' PAPI command on host " \
421 "{host}".format(host=node['host'], cmd=cmd)
423 with PapiExecutor(node) as papi_exec:
424 data = papi_exec.add(cli, **args).get_replies(err_msg). \
425 verify_reply(err_msg=err_msg)
428 logger.info("{cmd}:\n{data}".format(cmd=cmd, data=data["reply"]))
432 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
433 process_reply=True, ignore_errors=False,
435 """Execute the PAPI commands and check the return code.
436 Raise exception if the PAPI command(s) failed.
439 Do not use this method in L1 keywords. Use:
442 This method will be removed soon.
444 :param err_msg: The message used if the PAPI command(s) execution fails.
445 :param process_reply: Indicate whether or not to process PAPI reply.
446 :param ignore_errors: If true, the errors in the reply are ignored.
447 :param timeout: Timeout in seconds.
449 :type process_reply: bool
450 :type ignore_errors: bool
452 :returns: Papi response including: papi reply, stdout, stderr and
455 :raises AssertionError: If PAPI command(s) execution failed.
457 # TODO: Migrate callers to get_replies and delete this method.
458 return self.get_replies(
459 process_reply=process_reply, ignore_errors=ignore_errors,
460 err_msg=err_msg, timeout=timeout)
463 def _process_api_data(api_d):
464 """Process API data for smooth converting to JSON string.
466 Apply binascii.hexlify() method for string values.
468 :param api_d: List of APIs with their arguments.
470 :returns: List of APIs with arguments pre-processed for JSON.
474 def process_value(val):
477 :param val: Value to be processed.
479 :returns: Processed value.
480 :rtype: dict or str or int
482 if isinstance(val, dict):
484 for val_k, val_v in val.iteritems():
485 val_dict[str(val_k)] = process_value(val_v)
488 return binascii.hexlify(val) if isinstance(val, str) else val
490 api_data_processed = list()
492 api_args_processed = dict()
493 for a_k, a_v in api["api_args"].iteritems():
494 api_args_processed[str(a_k)] = process_value(a_v)
495 api_data_processed.append(dict(api_name=api["api_name"],
496 api_args=api_args_processed))
497 return api_data_processed
500 def _revert_api_reply(api_r):
501 """Process API reply / a part of API reply.
503 Apply binascii.unhexlify() method for unicode values.
505 TODO: Implement complex solution to process of replies.
507 :param api_r: API reply.
509 :returns: Processed API reply / a part of API reply.
514 for reply_key, reply_v in api_r.iteritems():
515 for a_k, a_v in reply_v.iteritems():
516 reply_value[a_k] = binascii.unhexlify(a_v) \
517 if isinstance(a_v, unicode) else a_v
518 reply_dict[reply_key] = reply_value
521 def _process_reply(self, api_reply):
522 """Process API reply.
524 :param api_reply: API reply.
525 :type api_reply: dict or list of dict
526 :returns: Processed API reply.
529 if isinstance(api_reply, list):
530 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
532 reverted_reply = self._revert_api_reply(api_reply)
533 return reverted_reply
535 def _execute_papi(self, api_data, method='request', err_msg="",
537 """Execute PAPI command(s) on remote node and store the result.
539 :param api_data: List of APIs with their arguments.
540 :param method: VPP Python API method. Supported methods are: 'request',
542 :param err_msg: The message used if the PAPI command(s) execution fails.
543 :param timeout: Timeout in seconds.
548 :returns: Stdout and stderr.
549 :rtype: 2-tuple of str
550 :raises SSHTimeout: If PAPI command(s) execution has timed out.
551 :raises RuntimeError: If PAPI executor failed due to another reason.
552 :raises AssertionError: If PAPI command(s) execution has failed.
556 RuntimeError("No API data provided.")
558 json_data = json.dumps(api_data) \
559 if method in ("stats", "stats_request") \
560 else json.dumps(self._process_api_data(api_data))
562 cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\
563 format(fw_dir=Constants.REMOTE_FW_DIR,
564 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
568 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
569 cmd=cmd, timeout=timeout, log_stdout_err=False)
571 logger.error("PAPI command(s) execution timeout on host {host}:"
572 "\n{apis}".format(host=self._node["host"],
576 raise RuntimeError("PAPI command(s) execution on host {host} "
577 "failed: {apis}".format(host=self._node["host"],
580 raise AssertionError(err_msg)
582 return stdout, stderr
584 def _execute(self, method='request', process_reply=True,
585 ignore_errors=False, err_msg="", timeout=120):
586 """Turn internal command list into proper data and execute; return
589 This method also clears the internal command list.
592 Do not use this method in L1 keywords. Use:
597 :param method: VPP Python API method. Supported methods are: 'request',
599 :param process_reply: Process PAPI reply if True.
600 :param ignore_errors: If true, the errors in the reply are ignored.
601 :param err_msg: The message used if the PAPI command(s) execution fails.
602 :param timeout: Timeout in seconds.
604 :type process_reply: bool
605 :type ignore_errors: bool
608 :returns: Papi response including: papi reply, stdout, stderr and
611 :raises KeyError: If the reply is not correct.
614 local_list = self._api_command_list
616 # Clear first as execution may fail.
617 self._api_command_list = list()
619 stdout, stderr = self._execute_papi(
620 local_list, method=method, err_msg=err_msg, timeout=timeout)
624 json_data = json.loads(stdout)
626 logger.error("An error occured while processing the PAPI "
627 "request:\n{rqst}".format(rqst=local_list))
629 for data in json_data:
631 api_reply_processed = dict(
632 api_name=data["api_name"],
633 api_reply=self._process_reply(data["api_reply"]))
639 papi_reply.append(api_reply_processed)
641 # Log processed papi reply to be able to check API replies changes
642 logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply))
645 papi_reply=papi_reply, stdout=stdout, stderr=stderr,
646 requests=[rqst["api_name"] for rqst in local_list])