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)
296 json_data = json.loads(stdout)
297 for data in json_data:
299 api_reply_processed = dict(
300 api_name=data["api_name"],
301 api_reply=self._process_reply(data["api_reply"]))
307 papi_reply.append(api_reply_processed)
309 return PapiResponse(papi_reply=papi_reply,
313 requests=[rqst["api_name"] for rqst in local_list])
315 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
316 process_reply=True, ignore_errors=False,
318 """Execute the PAPI commands and check the return code.
319 Raise exception if the PAPI command(s) failed.
321 :param err_msg: The message used if the PAPI command(s) execution fails.
322 :param process_reply: Indicate whether or not to process PAPI reply.
323 :param ignore_errors: If true, the errors in the reply are ignored.
324 :param timeout: Timeout in seconds.
326 :type process_reply: bool
327 :type ignore_errors: bool
329 :returns: Papi response including: papi reply, stdout, stderr and
332 :raises AssertionError: If PAPI command(s) execution failed.
335 response = self.execute(process_reply=process_reply,
336 ignore_errors=ignore_errors,
339 if response.ret_code != 0:
340 raise AssertionError(err_msg)
343 def execute_should_fail(self,
344 err_msg="Execution of PAPI command did not fail.",
345 process_reply=False, ignore_errors=False,
347 """Execute the PAPI commands and check the return code.
348 Raise exception if the PAPI command(s) did not fail.
350 It does not return anything as we expect it fails.
352 :param err_msg: The message used if the PAPI command(s) execution fails.
353 :param process_reply: Indicate whether or not to process PAPI reply.
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 :raises AssertionError: If PAPI command(s) execution passed.
363 response = self.execute(process_reply=process_reply,
364 ignore_errors=ignore_errors,
367 if response.ret_code == 0:
368 raise AssertionError(err_msg)
371 def _process_api_data(api_d):
372 """Process API data for smooth converting to JSON string.
374 Apply binascii.hexlify() method for string values.
376 :param api_d: List of APIs with their arguments.
378 :returns: List of APIs with arguments pre-processed for JSON.
382 api_data_processed = list()
384 api_args_processed = dict()
385 for a_k, a_v in api["api_args"].iteritems():
386 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
387 api_args_processed[str(a_k)] = value
388 api_data_processed.append(dict(api_name=api["api_name"],
389 api_args=api_args_processed))
390 return api_data_processed
393 def _revert_api_reply(api_r):
394 """Process API reply / a part of API reply.
396 Apply binascii.unhexlify() method for unicode values.
398 TODO: Remove the disabled code when definitely not needed.
400 :param api_r: API reply.
402 :returns: Processed API reply / a part of API reply.
408 for reply_key, reply_v in api_r.iteritems():
409 for a_k, a_v in reply_v.iteritems():
410 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
412 # reply_value[a_k] = value
413 reply_value[a_k] = a_v
414 reply_dict[reply_key] = reply_value
417 def _process_reply(self, api_reply):
418 """Process API reply.
420 :param api_reply: API reply.
421 :type api_reply: dict or list of dict
422 :returns: Processed API reply.
426 if isinstance(api_reply, list):
427 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
429 reverted_reply = self._revert_api_reply(api_reply)
430 return reverted_reply
432 def _execute_papi(self, api_data, timeout=120):
433 """Execute PAPI command(s) on remote node and store the result.
435 :param api_data: List of APIs with their arguments.
436 :param timeout: Timeout in seconds.
439 :raises SSHTimeout: If PAPI command(s) execution has timed out.
440 :raises RuntimeError: If PAPI executor failed due to another reason.
444 RuntimeError("No API data provided.")
446 api_data_processed = self._process_api_data(api_data)
447 json_data = json.dumps(api_data_processed)
449 cmd = "{fw_dir}/{papi_provider} --json_data '{json}'".format(
450 fw_dir=Constants.REMOTE_FW_DIR,
451 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
455 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
456 cmd=cmd, timeout=timeout)
458 logger.error("PAPI command(s) execution timeout on host {host}:"
459 "\n{apis}".format(host=self._node["host"],
463 raise RuntimeError("PAPI command(s) execution on host {host} "
464 "failed: {apis}".format(host=self._node["host"],
466 return ret_code, stdout, stderr