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", history=True, **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 history: Enable/disable adding command to PAPI command history.
301 :param kwargs: Optional key-value arguments.
302 :type csit_papi_command: str
305 :returns: self, so that method chaining is possible.
309 PapiHistory.add_to_papi_history(
310 self._node, csit_papi_command, **kwargs)
311 self._api_command_list.append(dict(api_name=csit_papi_command,
315 def get_stats(self, err_msg="Failed to get statistics.", timeout=120):
316 """Get VPP Stats from VPP Python API.
318 :param err_msg: The message used if the PAPI command(s) execution fails.
319 :param timeout: Timeout in seconds.
322 :returns: Requested VPP statistics.
326 paths = [cmd['api_args']['path'] for cmd in self._api_command_list]
327 self._api_command_list = list()
329 stdout, _ = self._execute_papi(
330 paths, method='stats', err_msg=err_msg, timeout=timeout)
332 return json.loads(stdout)
334 def get_stats_reply(self, err_msg="Failed to get statistics.", timeout=120):
335 """Get VPP Stats reply from VPP Python API.
337 :param err_msg: The message used if the PAPI command(s) execution fails.
338 :param timeout: Timeout in seconds.
341 :returns: Requested VPP statistics.
345 args = self._api_command_list[0]['api_args']
346 self._api_command_list = list()
348 stdout, _ = self._execute_papi(
349 args, method='stats_request', err_msg=err_msg, timeout=timeout)
351 return json.loads(stdout)
353 def get_replies(self, err_msg="Failed to get replies.",
354 process_reply=True, ignore_errors=False, timeout=120):
355 """Get reply/replies from VPP Python API.
357 :param err_msg: The message used if the PAPI command(s) execution fails.
358 :param process_reply: Process PAPI reply if True.
359 :param ignore_errors: If true, the errors in the reply are ignored.
360 :param timeout: Timeout in seconds.
362 :type process_reply: bool
363 :type ignore_errors: bool
365 :returns: Papi response including: papi reply, stdout, stderr and
369 return self._execute(
370 method='request', process_reply=process_reply,
371 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
373 def get_dump(self, err_msg="Failed to get dump.",
374 process_reply=True, ignore_errors=False, timeout=120):
375 """Get dump from VPP Python API.
377 :param err_msg: The message used if the PAPI command(s) execution fails.
378 :param process_reply: Process PAPI reply if True.
379 :param ignore_errors: If true, the errors in the reply are ignored.
380 :param timeout: Timeout in seconds.
382 :type process_reply: bool
383 :type ignore_errors: bool
385 :returns: Papi response including: papi reply, stdout, stderr and
389 return self._execute(
390 method='dump', process_reply=process_reply,
391 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
394 def dump_and_log(node, cmds):
395 """Dump and log requested information.
397 :param node: DUT node.
398 :param cmds: Dump commands to be executed.
402 with PapiExecutor(node) as papi_exec:
404 dump = papi_exec.add(cmd).get_dump()
405 logger.debug("{cmd}:\n{data}".format(
406 cmd=cmd, data=pformat(dump.reply[0]["api_reply"])))
409 def run_cli_cmd(node, cmd, log=True):
410 """Run a CLI command.
412 :param node: Node to run command on.
413 :param cmd: The CLI command to be run on the node.
414 :param log: If True, the response is logged.
418 :returns: Verified data from PAPI response.
424 err_msg = "Failed to run 'cli_inband {cmd}' PAPI command on host " \
425 "{host}".format(host=node['host'], cmd=cmd)
427 with PapiExecutor(node) as papi_exec:
428 data = papi_exec.add(cli, **args).get_replies(err_msg). \
429 verify_reply(err_msg=err_msg)
432 logger.info("{cmd}:\n{data}".format(cmd=cmd, data=data["reply"]))
436 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
437 process_reply=True, ignore_errors=False,
439 """Execute the PAPI commands and check the return code.
440 Raise exception if the PAPI command(s) failed.
443 Do not use this method in L1 keywords. Use:
446 This method will be removed soon.
448 :param err_msg: The message used if the PAPI command(s) execution fails.
449 :param process_reply: Indicate whether or not to process PAPI reply.
450 :param ignore_errors: If true, the errors in the reply are ignored.
451 :param timeout: Timeout in seconds.
453 :type process_reply: bool
454 :type ignore_errors: bool
456 :returns: Papi response including: papi reply, stdout, stderr and
459 :raises AssertionError: If PAPI command(s) execution failed.
461 # TODO: Migrate callers to get_replies and delete this method.
462 return self.get_replies(
463 process_reply=process_reply, ignore_errors=ignore_errors,
464 err_msg=err_msg, timeout=timeout)
467 def _process_api_data(api_d):
468 """Process API data for smooth converting to JSON string.
470 Apply binascii.hexlify() method for string values.
472 :param api_d: List of APIs with their arguments.
474 :returns: List of APIs with arguments pre-processed for JSON.
478 def process_value(val):
481 :param val: Value to be processed.
483 :returns: Processed value.
484 :rtype: dict or str or int
486 if isinstance(val, dict):
487 for val_k, val_v in val.iteritems():
488 val[str(val_k)] = process_value(val_v)
490 elif isinstance(val, list):
491 for idx, val_l in enumerate(val):
492 val[idx] = process_value(val_l)
495 return binascii.hexlify(val) if isinstance(val, str) else val
497 api_data_processed = list()
499 api_args_processed = dict()
500 for a_k, a_v in api["api_args"].iteritems():
501 api_args_processed[str(a_k)] = process_value(a_v)
502 api_data_processed.append(dict(api_name=api["api_name"],
503 api_args=api_args_processed))
504 return api_data_processed
507 def _revert_api_reply(api_r):
508 """Process API reply / a part of API reply.
510 Apply binascii.unhexlify() method for unicode values.
512 TODO: Implement complex solution to process of replies.
514 :param api_r: API reply.
516 :returns: Processed API reply / a part of API reply.
519 def process_value(val):
522 :param val: Value to be processed.
524 :returns: Processed value.
525 :rtype: dict or str or int
527 if isinstance(val, dict):
528 for val_k, val_v in val.iteritems():
529 val[str(val_k)] = process_value(val_v)
531 elif isinstance(val, list):
532 for idx, val_l in enumerate(val):
533 val[idx] = process_value(val_l)
535 elif isinstance(val, unicode):
536 return binascii.unhexlify(val)
542 for reply_key, reply_v in api_r.iteritems():
543 for a_k, a_v in reply_v.iteritems():
544 reply_value[a_k] = process_value(a_v)
545 reply_dict[reply_key] = reply_value
548 def _process_reply(self, api_reply):
549 """Process API reply.
551 :param api_reply: API reply.
552 :type api_reply: dict or list of dict
553 :returns: Processed API reply.
556 if isinstance(api_reply, list):
557 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
559 reverted_reply = self._revert_api_reply(api_reply)
560 return reverted_reply
562 def _execute_papi(self, api_data, method='request', err_msg="",
564 """Execute PAPI command(s) on remote node and store the result.
566 :param api_data: List of APIs with their arguments.
567 :param method: VPP Python API method. Supported methods are: 'request',
569 :param err_msg: The message used if the PAPI command(s) execution fails.
570 :param timeout: Timeout in seconds.
575 :returns: Stdout and stderr.
576 :rtype: 2-tuple of str
577 :raises SSHTimeout: If PAPI command(s) execution has timed out.
578 :raises RuntimeError: If PAPI executor failed due to another reason.
579 :raises AssertionError: If PAPI command(s) execution has failed.
583 RuntimeError("No API data provided.")
585 json_data = json.dumps(api_data) \
586 if method in ("stats", "stats_request") \
587 else json.dumps(self._process_api_data(api_data))
589 cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\
590 format(fw_dir=Constants.REMOTE_FW_DIR,
591 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
595 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
596 cmd=cmd, timeout=timeout, log_stdout_err=False)
598 logger.error("PAPI command(s) execution timeout on host {host}:"
599 "\n{apis}".format(host=self._node["host"],
603 raise RuntimeError("PAPI command(s) execution on host {host} "
604 "failed: {apis}".format(host=self._node["host"],
607 raise AssertionError(err_msg)
609 return stdout, stderr
611 def _execute(self, method='request', process_reply=True,
612 ignore_errors=False, err_msg="", timeout=120):
613 """Turn internal command list into proper data and execute; return
616 This method also clears the internal command list.
619 Do not use this method in L1 keywords. Use:
624 :param method: VPP Python API method. Supported methods are: 'request',
626 :param process_reply: Process PAPI reply if True.
627 :param ignore_errors: If true, the errors in the reply are ignored.
628 :param err_msg: The message used if the PAPI command(s) execution fails.
629 :param timeout: Timeout in seconds.
631 :type process_reply: bool
632 :type ignore_errors: bool
635 :returns: Papi response including: papi reply, stdout, stderr and
638 :raises KeyError: If the reply is not correct.
641 local_list = self._api_command_list
643 # Clear first as execution may fail.
644 self._api_command_list = list()
646 stdout, stderr = self._execute_papi(
647 local_list, method=method, err_msg=err_msg, timeout=timeout)
651 json_data = json.loads(stdout)
653 logger.error("An error occured while processing the PAPI "
654 "request:\n{rqst}".format(rqst=local_list))
656 for data in json_data:
658 api_reply_processed = dict(
659 api_name=data["api_name"],
660 api_reply=self._process_reply(data["api_reply"]))
666 papi_reply.append(api_reply_processed)
668 # Log processed papi reply to be able to check API replies changes
669 logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply))
672 papi_reply=papi_reply, stdout=stdout, stderr=stderr,
673 requests=[rqst["api_name"] for rqst in local_list])