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="", requests=None):
36 """Construct the Papi response by setting the values needed.
39 Implement 'dump' analogue of verify_replies that would concatenate
40 the values, so that call sites do not have to do that themselves.
42 :param papi_reply: API reply from last executed PAPI command(s).
43 :param stdout: stdout from last executed PAPI command(s).
44 :param stderr: stderr from last executed PAPI command(s).
45 :param requests: List of used PAPI requests. It is used while verifying
46 replies. If None, expected replies must be provided for verify_reply
47 and verify_replies methods.
48 :type papi_reply: list or None
54 # API reply from last executed PAPI command(s).
55 self.reply = papi_reply
57 # stdout from last executed PAPI command(s).
60 # stderr from last executed PAPI command(s).
63 # List of used PAPI requests.
64 self.requests = requests
66 # List of expected PAPI replies. It is used while verifying replies.
68 self.expected_replies = \
69 ["{rqst}_reply".format(rqst=rqst) for rqst in self.requests]
72 """Return string with human readable description of the PapiResponse.
74 :returns: Readable description.
78 "papi_reply={papi_reply},stdout={stdout},stderr={stderr},"
79 "requests={requests}").format(
80 papi_reply=self.reply, stdout=self.stdout, stderr=self.stderr,
81 requests=self.requests)
84 """Return string executable as Python constructor call.
86 :returns: Executable constructor call.
89 return "PapiResponse({str})".format(str=str(self))
91 def verify_reply(self, cmd_reply=None, idx=0,
92 err_msg="Failed to verify PAPI reply."):
93 """Verify and return data from the PAPI response.
95 Note: Use only with a simple request / reply command. In this case the
96 PAPI reply includes 'retval' which is checked in this method.
98 Do not use with 'dump' and 'vpp-stats' methods.
100 Use if PAPI response includes only one command reply.
102 Use it this way (preferred):
104 with PapiExecutor(node) as papi_exec:
105 data = papi_exec.add('show_version').get_replies().verify_reply()
107 or if you must provide the expected reply (not recommended):
109 with PapiExecutor(node) as papi_exec:
110 data = papi_exec.add('show_version').get_replies().\
111 verify_reply('show_version_reply')
113 :param cmd_reply: PAPI reply. If None, list of 'requests' should have
114 been provided to the __init__ method as pre-generated list of
115 replies is used in this method in this case.
116 The PapiExecutor._execute() method provides the requests
118 :param idx: Index to PapiResponse.reply list.
119 :param err_msg: The message used if the verification fails.
122 :type err_msg: str or None
123 :returns: Verified data from PAPI response.
125 :raises AssertionError: If the PAPI return value is not 0, so the reply
127 :raises KeyError, IndexError: If the reply does not have expected
130 cmd_rpl = self.expected_replies[idx] if cmd_reply is None else cmd_reply
132 data = self.reply[idx]['api_reply'][cmd_rpl]
133 if data['retval'] != 0:
134 raise AssertionError("{msg}\nidx={idx}, cmd_reply={reply}".
135 format(msg=err_msg, idx=idx, reply=cmd_rpl))
139 def verify_replies(self, cmd_replies=None,
140 err_msg="Failed to verify PAPI reply."):
141 """Verify and return data from the PAPI response.
143 Note: Use only with request / reply commands. In this case each
144 PAPI reply includes 'retval' which is checked.
146 Do not use with 'dump' and 'vpp-stats' methods.
148 Use if PAPI response includes more than one command reply.
152 with PapiExecutor(node) as papi_exec:
153 papi_exec.add(cmd1, **args1).add(cmd2, **args2).add(cmd2, **args3).\
154 get_replies(err_msg).verify_replies()
156 or if you need the data from the PAPI response:
158 with PapiExecutor(node) as papi_exec:
159 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
160 add(cmd2, **args3).get_replies(err_msg).verify_replies()
162 or if you must provide the list of expected replies (not recommended):
164 with PapiExecutor(node) as papi_exec:
165 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
166 add(cmd2, **args3).get_replies(err_msg).\
167 verify_replies(cmd_replies=cmd_replies)
169 :param cmd_replies: List of PAPI command replies. If None, list of
170 'requests' should have been provided to the __init__ method as
171 pre-generated list of replies is used in this method in this case.
172 The PapiExecutor._execute() method provides the requests
174 :param err_msg: The message used if the verification fails.
175 :type cmd_replies: list of str or None
177 :returns: List of verified data from PAPI response.
179 :raises AssertionError: If the PAPI response does not include at least
180 one of specified command replies.
184 cmd_rpls = self.expected_replies if cmd_replies is None else cmd_replies
186 if len(self.reply) != len(cmd_rpls):
187 raise AssertionError(err_msg)
188 for idx, cmd_reply in enumerate(cmd_rpls):
189 data.append(self.verify_reply(cmd_reply, idx, err_msg))
194 class PapiExecutor(object):
195 """Contains methods for executing VPP Python API commands on DUTs.
197 Note: Use only with "with" statement, e.g.:
199 with PapiExecutor(node) as papi_exec:
200 papi_resp = papi_exec.add('show_version').get_replies(err_msg)
202 This class processes three classes of VPP PAPI methods:
203 1. simple request / reply: method='request',
204 2. dump functions: method='dump',
205 3. vpp-stats: method='stats'.
207 The recommended ways of use are (examples):
209 1. Simple request / reply
211 a. One request with no arguments:
213 with PapiExecutor(node) as papi_exec:
214 data = papi_exec.add('show_version').get_replies().\
217 b. Three requests with arguments, the second and the third ones are the same
218 but with different arguments.
220 with PapiExecutor(node) as papi_exec:
221 data = papi_exec.add(cmd1, **args1).add(cmd2, **args2).\
222 add(cmd2, **args3).get_replies(err_msg).verify_replies()
226 cmd = 'sw_interface_rx_placement_dump'
227 with PapiExecutor(node) as papi_exec:
228 papi_resp = papi_exec.add(cmd, sw_if_index=ifc['vpp_sw_index']).\
233 path = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
235 with PapiExecutor(node) as papi_exec:
236 data = papi_exec.add(api_name='vpp-stats', path=path).get_stats()
238 print('RX interface core 0, sw_if_index 0:\n{0}'.\
239 format(data[0]['/if/rx'][0][0]))
244 path_2 = ['^/if', '/err/ip4-input', '/sys/node/ip4-input']
246 with PapiExecutor(node) as papi_exec:
247 data = papi_exec.add('vpp-stats', path=path_1).\
248 add('vpp-stats', path=path_2).get_stats()
250 print('RX interface core 0, sw_if_index 0:\n{0}'.\
251 format(data[1]['/if/rx'][0][0]))
253 Note: In this case, when PapiExecutor method 'add' is used:
254 - its parameter 'csit_papi_command' is used only to keep information
255 that vpp-stats are requested. It is not further processed but it is
256 included in the PAPI history this way:
257 vpp-stats(path=['^/if', '/err/ip4-input', '/sys/node/ip4-input'])
258 Always use csit_papi_command="vpp-stats" if the VPP PAPI method
260 - the second parameter must be 'path' as it is used by PapiExecutor
264 def __init__(self, node):
267 :param node: Node to run command(s) on.
271 # Node to run command(s) on.
274 # The list of PAPI commands to be executed on the node.
275 self._api_command_list = list()
281 self._ssh.connect(self._node)
283 raise RuntimeError("Cannot open SSH connection to host {host} to "
284 "execute PAPI command(s)".
285 format(host=self._node["host"]))
288 def __exit__(self, exc_type, exc_val, exc_tb):
289 self._ssh.disconnect(self._node)
291 def add(self, csit_papi_command="vpp-stats", **kwargs):
292 """Add next command to internal command list; return self.
294 The argument name 'csit_papi_command' must be unique enough as it cannot
295 be repeated in kwargs.
297 :param csit_papi_command: VPP API command.
298 :param kwargs: Optional key-value arguments.
299 :type csit_papi_command: str
301 :returns: self, so that method chaining is possible.
304 PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
305 self._api_command_list.append(dict(api_name=csit_papi_command,
309 def get_stats(self, err_msg="Failed to get statistics.", timeout=120):
310 """Get VPP Stats from VPP Python API.
312 :param err_msg: The message used if the PAPI command(s) execution fails.
313 :param timeout: Timeout in seconds.
316 :returns: Requested VPP statistics.
320 paths = [cmd['api_args']['path'] for cmd in self._api_command_list]
321 self._api_command_list = list()
323 stdout, _ = self._execute_papi(
324 paths, method='stats', err_msg=err_msg, timeout=timeout)
326 return json.loads(stdout)
328 def get_replies(self, err_msg="Failed to get replies.",
329 process_reply=True, ignore_errors=False, timeout=120):
330 """Get reply/replies from VPP Python API.
332 :param err_msg: The message used if the PAPI command(s) execution fails.
333 :param process_reply: Process PAPI reply if True.
334 :param ignore_errors: If true, the errors in the reply are ignored.
335 :param timeout: Timeout in seconds.
337 :type process_reply: bool
338 :type ignore_errors: bool
340 :returns: Papi response including: papi reply, stdout, stderr and
344 return self._execute(
345 method='request', process_reply=process_reply,
346 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
348 def get_dump(self, err_msg="Failed to get dump.",
349 process_reply=True, ignore_errors=False, timeout=120):
350 """Get dump from VPP Python API.
352 :param err_msg: The message used if the PAPI command(s) execution fails.
353 :param process_reply: Process PAPI reply if True.
354 :param ignore_errors: If true, the errors in the reply are ignored.
355 :param timeout: Timeout in seconds.
357 :type process_reply: bool
358 :type ignore_errors: bool
360 :returns: Papi response including: papi reply, stdout, stderr and
364 return self._execute(
365 method='dump', process_reply=process_reply,
366 ignore_errors=ignore_errors, err_msg=err_msg, timeout=timeout)
368 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
369 process_reply=True, ignore_errors=False,
371 """Execute the PAPI commands and check the return code.
372 Raise exception if the PAPI command(s) failed.
375 Do not use this method in L1 keywords. Use:
378 This method will be removed soon.
380 :param err_msg: The message used if the PAPI command(s) execution fails.
381 :param process_reply: Indicate whether or not to process PAPI reply.
382 :param ignore_errors: If true, the errors in the reply are ignored.
383 :param timeout: Timeout in seconds.
385 :type process_reply: bool
386 :type ignore_errors: bool
388 :returns: Papi response including: papi reply, stdout, stderr and
391 :raises AssertionError: If PAPI command(s) execution failed.
393 # TODO: Migrate callers to get_replies and delete this method.
394 return self.get_replies(
395 process_reply=process_reply, ignore_errors=ignore_errors,
396 err_msg=err_msg, timeout=timeout)
399 def _process_api_data(api_d):
400 """Process API data for smooth converting to JSON string.
402 Apply binascii.hexlify() method for string values.
404 :param api_d: List of APIs with their arguments.
406 :returns: List of APIs with arguments pre-processed for JSON.
410 api_data_processed = list()
412 api_args_processed = dict()
413 for a_k, a_v in api["api_args"].iteritems():
414 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
415 api_args_processed[str(a_k)] = value
416 api_data_processed.append(dict(api_name=api["api_name"],
417 api_args=api_args_processed))
418 return api_data_processed
421 def _revert_api_reply(api_r):
422 """Process API reply / a part of API reply.
424 Apply binascii.unhexlify() method for unicode values.
426 TODO: Implement complex solution to process of replies.
428 :param api_r: API reply.
430 :returns: Processed API reply / a part of API reply.
435 for reply_key, reply_v in api_r.iteritems():
436 for a_k, a_v in reply_v.iteritems():
437 reply_value[a_k] = binascii.unhexlify(a_v) \
438 if isinstance(a_v, unicode) else a_v
439 reply_dict[reply_key] = reply_value
442 def _process_reply(self, api_reply):
443 """Process API reply.
445 :param api_reply: API reply.
446 :type api_reply: dict or list of dict
447 :returns: Processed API reply.
450 if isinstance(api_reply, list):
451 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
453 reverted_reply = self._revert_api_reply(api_reply)
454 return reverted_reply
456 def _execute_papi(self, api_data, method='request', err_msg="",
458 """Execute PAPI command(s) on remote node and store the result.
460 :param api_data: List of APIs with their arguments.
461 :param method: VPP Python API method. Supported methods are: 'request',
463 :param err_msg: The message used if the PAPI command(s) execution fails.
464 :param timeout: Timeout in seconds.
469 :returns: Stdout and stderr.
470 :rtype: 2-tuple of str
471 :raises SSHTimeout: If PAPI command(s) execution has timed out.
472 :raises RuntimeError: If PAPI executor failed due to another reason.
473 :raises AssertionError: If PAPI command(s) execution has failed.
477 RuntimeError("No API data provided.")
479 json_data = json.dumps(api_data) if method == "stats" \
480 else json.dumps(self._process_api_data(api_data))
482 cmd = "{fw_dir}/{papi_provider} --method {method} --data '{json}'".\
483 format(fw_dir=Constants.REMOTE_FW_DIR,
484 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
488 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
489 cmd=cmd, timeout=timeout)
491 logger.error("PAPI command(s) execution timeout on host {host}:"
492 "\n{apis}".format(host=self._node["host"],
496 raise RuntimeError("PAPI command(s) execution on host {host} "
497 "failed: {apis}".format(host=self._node["host"],
500 raise AssertionError(err_msg)
502 return stdout, stderr
504 def _execute(self, method='request', process_reply=True,
505 ignore_errors=False, err_msg="", timeout=120):
506 """Turn internal command list into proper data and execute; return
509 This method also clears the internal command list.
512 Do not use this method in L1 keywords. Use:
517 :param method: VPP Python API method. Supported methods are: 'request',
519 :param process_reply: Process PAPI reply if True.
520 :param ignore_errors: If true, the errors in the reply are ignored.
521 :param err_msg: The message used if the PAPI command(s) execution fails.
522 :param timeout: Timeout in seconds.
524 :type process_reply: bool
525 :type ignore_errors: bool
528 :returns: Papi response including: papi reply, stdout, stderr and
531 :raises KeyError: If the reply is not correct.
534 local_list = self._api_command_list
536 # Clear first as execution may fail.
537 self._api_command_list = list()
539 stdout, stderr = self._execute_papi(
540 local_list, method=method, err_msg=err_msg, timeout=timeout)
544 json_data = json.loads(stdout)
546 logger.error("An error occured while processing the PAPI "
547 "request:\n{rqst}".format(rqst=local_list))
549 for data in json_data:
551 api_reply_processed = dict(
552 api_name=data["api_name"],
553 api_reply=self._process_reply(data["api_reply"]))
559 papi_reply.append(api_reply_processed)
561 # Log processed papi reply to be able to check API replies changes
562 logger.debug("Processed PAPI reply: {reply}".format(reply=papi_reply))
565 papi_reply=papi_reply, stdout=stdout, stderr=stderr,
566 requests=[rqst["api_name"] for rqst in local_list])