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.
16 This version supports only simple request / reply VPP API methods.
28 from robot.api import logger
30 from resources.libraries.python.Constants import Constants
31 from resources.libraries.python.ssh import SSH, SSHTimeout
32 from resources.libraries.python.PapiHistory import PapiHistory
35 __all__ = ["PapiExecutor", "PapiResponse"]
38 class PapiResponse(object):
39 """Class for metadata specifying the Papi reply, stdout, stderr and return
43 def __init__(self, papi_reply=None, stdout="", stderr="", ret_code=None,
45 """Construct the Papi response by setting the values needed.
47 :param papi_reply: API reply from last executed PAPI command(s).
48 :param stdout: stdout from last executed PAPI command(s).
49 :param stderr: stderr from last executed PAPI command(s).
50 :param ret_code: ret_code from last executed PAPI command(s).
51 :param requests: List of used PAPI requests. It is used while verifying
52 replies. If None, expected replies must be provided for verify_reply
53 and verify_replies methods.
54 :type papi_reply: list
61 # API reply from last executed PAPI command(s).
62 self.reply = papi_reply
64 # stdout from last executed PAPI command(s).
67 # stderr from last executed PAPI command(s).
70 # return code from last executed PAPI command(s).
71 self.ret_code = ret_code
73 # List of used PAPI requests.
74 self.requests = requests
76 # List of expected PAPI replies. It is used while verifying replies.
78 self.expected_replies = \
79 ["{rqst}_reply".format(rqst=rqst) for rqst in self.requests]
82 """Return string with human readable description of the PapiResponse.
84 :returns: Readable description.
87 return ("papi_reply={papi_reply},"
90 "ret_code={ret_code},"
91 "requests={requests}".
92 format(papi_reply=self.reply,
95 ret_code=self.ret_code,
96 requests=self.requests))
99 """Return string executable as Python constructor call.
101 :returns: Executable constructor call.
104 return "PapiResponse({str})".format(str=str(self))
106 def verify_reply(self, cmd_reply=None, idx=0,
107 err_msg="Failed to verify PAPI reply."):
108 """Verify and return data from the PAPI response.
110 Note: Use only with a simple request / reply command. In this case the
111 PAPI reply includes 'retval' which is checked in this method.
113 Use if PAPI response includes only one command reply.
115 Use it this way (preferred):
117 with PapiExecutor(node) as papi_exec:
118 data = papi_exec.add('show_version').execute_should_pass().\
121 or if you must provide the expected reply (not recommended):
123 with PapiExecutor(node) as papi_exec:
124 data = papi_exec.add('show_version').execute_should_pass().\
125 verify_reply('show_version_reply')
127 :param cmd_reply: PAPI reply. If None, list of 'requests' should have
128 been provided to the __init__ method as pre-generated list of
129 replies is used in this method in this case.
130 The .execute* methods are providing the requests automatically.
131 :param idx: Index to PapiResponse.reply list.
132 :param err_msg: The message used if the verification fails.
135 :type err_msg: str or None
136 :returns: Verified data from PAPI response.
138 :raises AssertionError: If the PAPI return value is not 0, so the reply
140 :raises KeyError, IndexError: If the reply does not have expected
143 cmd_rpl = self.expected_replies[idx] if cmd_reply is None else cmd_reply
145 data = self.reply[idx]['api_reply'][cmd_rpl]
146 if data['retval'] != 0:
147 raise AssertionError("{msg}\nidx={idx}, cmd_reply={reply}".
148 format(msg=err_msg, idx=idx, reply=cmd_rpl))
152 def verify_replies(self, cmd_replies=None,
153 err_msg="Failed to verify PAPI reply."):
154 """Verify and return data from the PAPI response.
156 Note: Use only with request / reply commands. In this case each
157 PAPI reply includes 'retval' which is checked.
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 execute_should_pass(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).execute_should_pass(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).execute_should_pass(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 .execute* methods are providing the requests automatically.
184 :param err_msg: The message used if the verification fails.
185 :type cmd_replies: list of str or None
187 :returns: List of verified data from PAPI response.
189 :raises AssertionError: If the PAPI response does not include at least
190 one of specified command replies.
194 cmd_rpls = self.expected_replies if cmd_replies is None else cmd_replies
196 if len(self.reply) != len(cmd_rpls):
197 raise AssertionError(err_msg)
198 for idx, cmd_reply in enumerate(cmd_rpls):
199 data.append(self.verify_reply(cmd_reply, idx, err_msg))
204 class PapiExecutor(object):
205 """Contains methods for executing Python API commands on DUTs.
207 Use only with "with" statement, e.g.:
209 with PapiExecutor(node) as papi_exec:
210 papi_resp = papi_exec.add('show_version').execute_should_pass(err_msg)
213 def __init__(self, node):
216 :param node: Node to run command(s) on.
220 # Node to run command(s) on.
223 # The list of PAPI commands to be executed on the node.
224 self._api_command_list = list()
230 self._ssh.connect(self._node)
232 raise RuntimeError("Cannot open SSH connection to host {host} to "
233 "execute PAPI command(s)".
234 format(host=self._node["host"]))
237 def __exit__(self, exc_type, exc_val, exc_tb):
238 self._ssh.disconnect(self._node)
241 """Empty the internal command list; return self.
243 Use when not sure whether previous usage has left something in the list.
245 :returns: self, so that method chaining is possible.
248 self._api_command_list = list()
251 def add(self, csit_papi_command, **kwargs):
252 """Add next command to internal command list; return self.
254 The argument name 'csit_papi_command' must be unique enough as it cannot
255 be repeated in kwargs.
257 :param csit_papi_command: VPP API command.
258 :param kwargs: Optional key-value arguments.
259 :type csit_papi_command: str
261 :returns: self, so that method chaining is possible.
264 PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
265 self._api_command_list.append(dict(api_name=csit_papi_command,
269 def execute(self, process_reply=True, ignore_errors=False, timeout=120):
270 """Turn internal command list into proper data and execute; return
273 This method also clears the internal command list.
275 :param process_reply: Process PAPI reply if True.
276 :param ignore_errors: If true, the errors in the reply are ignored.
277 :param timeout: Timeout in seconds.
278 :type process_reply: bool
279 :type ignore_errors: bool
281 :returns: Papi response including: papi reply, stdout, stderr and
284 :raises KeyError: If the reply is not correct.
287 local_list = self._api_command_list
289 # Clear first as execution may fail.
292 ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
297 json_data = json.loads(stdout)
299 logger.error("An error occured while processing the PAPI "
300 "request:\n{rqst}".format(rqst=local_list))
302 for data in json_data:
304 api_reply_processed = dict(
305 api_name=data["api_name"],
306 api_reply=self._process_reply(data["api_reply"]))
312 papi_reply.append(api_reply_processed)
314 return PapiResponse(papi_reply=papi_reply,
318 requests=[rqst["api_name"] for rqst in local_list])
320 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
321 process_reply=True, ignore_errors=False,
323 """Execute the PAPI commands and check the return code.
324 Raise exception if the PAPI command(s) failed.
326 :param err_msg: The message used if the PAPI command(s) execution fails.
327 :param process_reply: Indicate whether or not to process PAPI reply.
328 :param ignore_errors: If true, the errors in the reply are ignored.
329 :param timeout: Timeout in seconds.
331 :type process_reply: bool
332 :type ignore_errors: bool
334 :returns: Papi response including: papi reply, stdout, stderr and
337 :raises AssertionError: If PAPI command(s) execution failed.
340 response = self.execute(process_reply=process_reply,
341 ignore_errors=ignore_errors,
344 if response.ret_code != 0:
345 raise AssertionError(err_msg)
348 def execute_should_fail(self,
349 err_msg="Execution of PAPI command did not fail.",
350 process_reply=False, ignore_errors=False,
352 """Execute the PAPI commands and check the return code.
353 Raise exception if the PAPI command(s) did not fail.
355 It does not return anything as we expect it fails.
357 :param err_msg: The message used if the PAPI command(s) execution fails.
358 :param process_reply: Indicate whether or not to process PAPI reply.
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 :raises AssertionError: If PAPI command(s) execution passed.
368 response = self.execute(process_reply=process_reply,
369 ignore_errors=ignore_errors,
372 if response.ret_code == 0:
373 raise AssertionError(err_msg)
376 def _process_api_data(api_d):
377 """Process API data for smooth converting to JSON string.
379 Apply binascii.hexlify() method for string values.
381 :param api_d: List of APIs with their arguments.
383 :returns: List of APIs with arguments pre-processed for JSON.
387 api_data_processed = list()
389 api_args_processed = dict()
390 for a_k, a_v in api["api_args"].iteritems():
391 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
392 api_args_processed[str(a_k)] = value
393 api_data_processed.append(dict(api_name=api["api_name"],
394 api_args=api_args_processed))
395 return api_data_processed
398 def _revert_api_reply(api_r):
399 """Process API reply / a part of API reply.
401 Apply binascii.unhexlify() method for unicode values.
403 TODO: Remove the disabled code when definitely not needed.
405 :param api_r: API reply.
407 :returns: Processed API reply / a part of API reply.
413 for reply_key, reply_v in api_r.iteritems():
414 for a_k, a_v in reply_v.iteritems():
415 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
417 # reply_value[a_k] = value
418 reply_value[a_k] = a_v
419 reply_dict[reply_key] = reply_value
422 def _process_reply(self, api_reply):
423 """Process API reply.
425 :param api_reply: API reply.
426 :type api_reply: dict or list of dict
427 :returns: Processed API reply.
431 if isinstance(api_reply, list):
432 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
434 reverted_reply = self._revert_api_reply(api_reply)
435 return reverted_reply
437 def _execute_papi(self, api_data, timeout=120):
438 """Execute PAPI command(s) on remote node and store the result.
440 :param api_data: List of APIs with their arguments.
441 :param timeout: Timeout in seconds.
444 :raises SSHTimeout: If PAPI command(s) execution has timed out.
445 :raises RuntimeError: If PAPI executor failed due to another reason.
449 RuntimeError("No API data provided.")
451 api_data_processed = self._process_api_data(api_data)
452 json_data = json.dumps(api_data_processed)
454 cmd = "{fw_dir}/{papi_provider} --json_data '{json}'".format(
455 fw_dir=Constants.REMOTE_FW_DIR,
456 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
460 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
461 cmd=cmd, timeout=timeout)
463 logger.error("PAPI command(s) execution timeout on host {host}:"
464 "\n{apis}".format(host=self._node["host"],
468 raise RuntimeError("PAPI command(s) execution on host {host} "
469 "failed: {apis}".format(host=self._node["host"],
471 return ret_code, stdout, stderr