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 robot.api import logger
22 from resources.libraries.python.Constants import Constants
23 from resources.libraries.python.ssh import SSH, SSHTimeout
24 from resources.libraries.python.PapiHistory import PapiHistory
27 __all__ = ["PapiExecutor", "PapiResponse"]
30 class PapiResponse(object):
31 """Class for metadata specifying the Papi reply, stdout, stderr and return
35 def __init__(self, papi_reply=None, stdout="", stderr="", ret_code=None,
37 """Construct the Papi response by setting the values needed.
40 Implement 'dump' analogue of verify_replies that would concatenate
41 the values, so that call sites do not have to do that themselves.
43 :param papi_reply: API reply from last executed PAPI command(s).
44 :param stdout: stdout from last executed PAPI command(s).
45 :param stderr: stderr from last executed PAPI command(s).
46 :param ret_code: ret_code 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
57 # API reply from last executed PAPI command(s).
58 self.reply = papi_reply
60 # stdout from last executed PAPI command(s).
63 # stderr from last executed PAPI command(s).
66 # return code from last executed PAPI command(s).
67 self.ret_code = ret_code
69 # List of used PAPI requests.
70 self.requests = requests
72 # List of expected PAPI replies. It is used while verifying replies.
74 self.expected_replies = \
75 ["{rqst}_reply".format(rqst=rqst) for rqst in self.requests]
78 """Return string with human readable description of the PapiResponse.
80 :returns: Readable description.
83 return ("papi_reply={papi_reply},"
86 "ret_code={ret_code},"
87 "requests={requests}".
88 format(papi_reply=self.reply,
91 ret_code=self.ret_code,
92 requests=self.requests))
95 """Return string executable as Python constructor call.
97 :returns: Executable constructor call.
100 return "PapiResponse({str})".format(str=str(self))
102 def verify_reply(self, cmd_reply=None, idx=0,
103 err_msg="Failed to verify PAPI reply."):
104 """Verify and return data from the PAPI response.
106 Note: Use only with a simple request / reply command. In this case the
107 PAPI reply includes 'retval' which is checked in this method.
109 Do not use with 'dump' and 'vpp-stats' methods.
111 Use if PAPI response includes only one command reply.
113 Use it this way (preferred):
115 with PapiExecutor(node) as papi_exec:
116 data = papi_exec.add('show_version').get_replies().verify_reply()
118 or if you must provide the expected reply (not recommended):
120 with PapiExecutor(node) as papi_exec:
121 data = papi_exec.add('show_version').get_replies().\
122 verify_reply('show_version_reply')
124 :param cmd_reply: PAPI reply. If None, list of 'requests' should have
125 been provided to the __init__ method as pre-generated list of
126 replies is used in this method in this case.
127 The PapiExecutor._execute() method provides the requests
129 :param idx: Index to PapiResponse.reply list.
130 :param err_msg: The message used if the verification fails.
133 :type err_msg: str or None
134 :returns: Verified data from PAPI response.
136 :raises AssertionError: If the PAPI return value is not 0, so the reply
138 :raises KeyError, IndexError: If the reply does not have expected
141 cmd_rpl = self.expected_replies[idx] if cmd_reply is None else cmd_reply
143 data = self.reply[idx]['api_reply'][cmd_rpl]
144 if data['retval'] != 0:
145 raise AssertionError("{msg}\nidx={idx}, cmd_reply={reply}".
146 format(msg=err_msg, idx=idx, reply=cmd_rpl))
150 def verify_replies(self, cmd_replies=None,
151 err_msg="Failed to verify PAPI reply."):
152 """Verify and return data from the PAPI response.
154 Note: Use only with request / reply commands. In this case each
155 PAPI reply includes 'retval' which is checked.
157 Do not use with 'dump' and 'vpp-stats' methods.
159 Use if PAPI response includes more than one command reply.
163 with PapiExecutor(node) as papi_exec:
164 papi_exec.add(cmd1, **args1).add(cmd2, **args2).add(cmd2, **args3).\
165 get_replies(err_msg).verify_replies()
167 or if you need the data from the PAPI response:
169 with PapiExecutor(node) as papi_exec:
170 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
171 add(cmd2, **args3).get_replies(err_msg).verify_replies()
173 or if you must provide the list of expected replies (not recommended):
175 with PapiExecutor(node) as papi_exec:
176 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
177 add(cmd2, **args3).get_replies(err_msg).\
178 verify_replies(cmd_replies=cmd_replies)
180 :param cmd_replies: List of PAPI command replies. If None, list of
181 'requests' should have been provided to the __init__ method as
182 pre-generated list of replies is used in this method in this case.
183 The PapiExecutor._execute() method provides the requests
185 :param err_msg: The message used if the verification fails.
186 :type cmd_replies: list of str or None
188 :returns: List of verified data from PAPI response.
190 :raises AssertionError: If the PAPI response does not include at least
191 one of specified command replies.
195 cmd_rpls = self.expected_replies if cmd_replies is None else cmd_replies
197 if len(self.reply) != len(cmd_rpls):
198 raise AssertionError(err_msg)
199 for idx, cmd_reply in enumerate(cmd_rpls):
200 data.append(self.verify_reply(cmd_reply, idx, err_msg))
205 class PapiExecutor(object):
206 """Contains methods for executing VPP Python API commands on DUTs.
208 Note: Use only with "with" statement, e.g.:
210 with PapiExecutor(node) as papi_exec:
211 papi_resp = papi_exec.add('show_version').get_replies(err_msg)
213 This class processes three classes of VPP PAPI methods:
214 1. simple request / reply: method='request',
215 2. dump functions: method='dump',
216 3. vpp-stats: method='stats'.
218 The recommended ways of use are (examples):
220 1. Simple request / reply
222 a. One request with no arguments:
224 with PapiExecutor(node) as papi_exec:
225 data = papi_exec.add('show_version').get_replies().\
228 b. Three requests with arguments, the second and the third ones are the same
229 but with different arguments.
231 with PapiExecutor(node) as papi_exec:
232 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
233 add(cmd2, **args3).get_replies(err_msg).verify_replies()
237 cmd = 'sw_interface_rx_placement_dump'
238 with PapiExecutor(node) as papi_exec:
239 papi_resp = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
244 path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
246 with PapiExecutor(node) as papi_exec:
247 data = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
249 print('RX interface core 0, sw_if_index 0:\n{0}'.\
250 format(data[0]['/if/rx'][0][0]))
255 path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
257 with PapiExecutor(node) as papi_exec:
258 data = papi_exec.add('vpp-stats', path=path_1).\
259 add('vpp-stats', path=path_2).get_stats()
261 print('RX interface core 0, sw_if_index 0:\n{0}'.\
262 format(data[1]['/if/rx'][0][0]))
264 Note: In this case, when PapiExecutor method 'add' is used:
265 - its parameter 'csit_papi_command' is used only to keep information
266 that vpp-stats are requested. It is not further processed but it is
267 included in the PAPI history this way:
268 vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
269 Always use csit_papi_command="vpp-stats" if the VPP PAPI method
271 - the second parameter must be 'path' as it is used by PapiExecutor
275 def __init__(self, node):
278 :param node: Node to run command(s) on.
282 # Node to run command(s) on.
285 # The list of PAPI commands to be executed on the node.
286 self._api_command_list = list()
292 self._ssh.connect(self._node)
294 raise RuntimeError("Cannot open SSH connection to host {host} to "
295 "execute PAPI command(s)".
296 format(host=self._node["host"]))
299 def __exit__(self, exc_type, exc_val, exc_tb):
300 self._ssh.disconnect(self._node)
302 def add(self, csit_papi_command="vpp-stats", **kwargs):
303 """Add next command to internal command list; return self.
305 The argument name 'csit_papi_command' must be unique enough as it cannot
306 be repeated in kwargs.
308 :param csit_papi_command: VPP API command.
309 :param kwargs: Optional key-value arguments.
310 :type csit_papi_command: str
312 :returns: self, so that method chaining is possible.
315 PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
316 self._api_command_list.append(dict(api_name=csit_papi_command,
320 def get_stats(self, err_msg="Failed to get statistics.", timeout=120):
321 """Get VPP Stats from VPP Python API.
323 :param err_msg: The message used if the PAPI command(s) execution fails.
324 :param timeout: Timeout in seconds.
327 :returns: Requested VPP statistics.
331 paths = [cmd['api_args']['path'] for cmd in self._api_command_list]
332 self._api_command_list = list()
334 ret_code, stdout, _ = self._execute_papi(paths,
339 return json.loads(stdout)
341 def get_replies(self, err_msg="Failed to get replies.",
342 process_reply=True, ignore_errors=False, timeout=120):
343 """Get reply/replies from VPP Python API.
345 :param err_msg: The message used if the PAPI command(s) execution fails.
346 :param process_reply: Process PAPI reply if True.
347 :param ignore_errors: If true, the errors in the reply are ignored.
348 :param timeout: Timeout in seconds.
350 :type process_reply: bool
351 :type ignore_errors: bool
353 :returns: Papi response including: papi reply, stdout, stderr and
357 response = self._execute(method='request',
358 process_reply=process_reply,
359 ignore_errors=ignore_errors,
364 def get_dump(self, err_msg="Failed to get dump.",
365 process_reply=True, ignore_errors=False, timeout=120):
366 """Get dump from VPP Python API.
368 :param err_msg: The message used if the PAPI command(s) execution fails.
369 :param process_reply: Process PAPI reply if True.
370 :param ignore_errors: If true, the errors in the reply are ignored.
371 :param timeout: Timeout in seconds.
373 :type process_reply: bool
374 :type ignore_errors: bool
376 :returns: Papi response including: papi reply, stdout, stderr and
381 response = self._execute(method='dump',
382 process_reply=process_reply,
383 ignore_errors=ignore_errors,
388 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
389 process_reply=True, ignore_errors=False,
391 """Execute the PAPI commands and check the return code.
392 Raise exception if the PAPI command(s) failed.
395 Do not use this method in L1 keywords. Use:
398 This method will be removed soon.
400 :param err_msg: The message used if the PAPI command(s) execution fails.
401 :param process_reply: Indicate whether or not to process PAPI reply.
402 :param ignore_errors: If true, the errors in the reply are ignored.
403 :param timeout: Timeout in seconds.
405 :type process_reply: bool
406 :type ignore_errors: bool
408 :returns: Papi response including: papi reply, stdout, stderr and
411 :raises AssertionError: If PAPI command(s) execution failed.
414 response = self.get_replies(process_reply=process_reply,
415 ignore_errors=ignore_errors,
421 def _process_api_data(api_d):
422 """Process API data for smooth converting to JSON string.
424 Apply binascii.hexlify() method for string values.
426 :param api_d: List of APIs with their arguments.
428 :returns: List of APIs with arguments pre-processed for JSON.
432 api_data_processed = list()
434 api_args_processed = dict()
435 for a_k, a_v in api["api_args"].iteritems():
436 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
437 api_args_processed[str(a_k)] = value
438 api_data_processed.append(dict(api_name=api["api_name"],
439 api_args=api_args_processed))
440 return api_data_processed
443 def _revert_api_reply(api_r):
444 """Process API reply / a part of API reply.
446 Apply binascii.unhexlify() method for unicode values.
448 TODO: Implement complex solution to process of replies.
450 :param api_r: API reply.
452 :returns: Processed API reply / a part of API reply.
458 for reply_key, reply_v in api_r.iteritems():
459 for a_k, a_v in reply_v.iteritems():
460 reply_value[a_k] = binascii.unhexlify(a_v) \
461 if isinstance(a_v, unicode) else a_v
462 reply_dict[reply_key] = reply_value
465 def _process_reply(self, api_reply):
466 """Process API reply.
468 :param api_reply: API reply.
469 :type api_reply: dict or list of dict
470 :returns: Processed API reply.
474 if isinstance(api_reply, list):
475 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
477 reverted_reply = self._revert_api_reply(api_reply)
478 return reverted_reply
480 def _execute_papi(self, api_data, method='request', err_msg="",
482 """Execute PAPI command(s) on remote node and store the result.
484 :param api_data: List of APIs with their arguments.
485 :param method: VPP Python API method. Supported methods are: 'request',
487 :param err_msg: The message used if the PAPI command(s) execution fails.
488 :param timeout: Timeout in seconds.
493 :raises SSHTimeout: If PAPI command(s) execution has timed out.
494 :raises RuntimeError: If PAPI executor failed due to another reason.
495 :raises AssertionError: If PAPI command(s) execution has failed.
499 RuntimeError("No API data provided.")
501 json_data = json.dumps(api_data) if method == "stats" \
502 else json.dumps(self._process_api_data(api_data))
504 cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\
505 format(fw_dir=Constants.REMOTE_FW_DIR,
506 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
510 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
511 cmd=cmd, timeout=timeout)
513 logger.error("PAPI command(s) execution timeout on host {host}:"
514 "\n{apis}".format(host=self._node["host"],
518 raise RuntimeError("PAPI command(s) execution on host {host} "
519 "failed: {apis}".format(host=self._node["host"],
522 raise AssertionError(err_msg)
524 return ret_code, stdout, stderr
526 def _execute(self, method='request', process_reply=True,
527 ignore_errors=False, err_msg="", timeout=120):
528 """Turn internal command list into proper data and execute; return
531 This method also clears the internal command list.
534 Do not use this method in L1 keywords. Use:
539 :param method: VPP Python API method. Supported methods are: 'request',
541 :param process_reply: Process PAPI reply if True.
542 :param ignore_errors: If true, the errors in the reply are ignored.
543 :param err_msg: The message used if the PAPI command(s) execution fails.
544 :param timeout: Timeout in seconds.
546 :type process_reply: bool
547 :type ignore_errors: bool
550 :returns: Papi response including: papi reply, stdout, stderr and
553 :raises KeyError: If the reply is not correct.
556 local_list = self._api_command_list
558 # Clear first as execution may fail.
559 self._api_command_list = list()
561 ret_code, stdout, stderr = self._execute_papi(local_list,
568 json_data = json.loads(stdout)
570 logger.error("An error occured while processing the PAPI "
571 "request:\n{rqst}".format(rqst=local_list))
573 for data in json_data:
575 api_reply_processed = dict(
576 api_name=data["api_name"],
577 api_reply=self._process_reply(data["api_reply"]))
583 papi_reply.append(api_reply_processed)
585 # Log processed papi reply to be able to check API replies changes
586 logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply))
588 return PapiResponse(papi_reply=papi_reply,
592 requests=[rqst["api_name"] for rqst in local_list])