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."""
19 from robot.api import logger
21 from resources.libraries.python.Constants import Constants
22 from resources.libraries.python.ssh import SSH, SSHTimeout
23 from resources.libraries.python.PapiHistory import PapiHistory
25 __all__ = ["PapiExecutor", "PapiResponse"]
28 class PapiResponse(object):
29 """Class for metadata specifying the Papi reply, stdout, stderr and return
33 def __init__(self, papi_reply=None, stdout="", stderr="", ret_code=None):
34 """Construct the Papi response by setting the values needed.
36 :param papi_reply: API reply from last executed PAPI command(s).
37 :param stdout: stdout from last executed PAPI command(s).
38 :param stderr: stderr from last executed PAPI command(s).
39 :param ret_code: ret_code from last executed PAPI command(s).
40 :type papi_reply: list
46 # API reply from last executed PAPI command(s)
47 self.reply = papi_reply
49 # stdout from last executed PAPI command(s)
52 # stderr from last executed PAPI command(s).
55 # return code from last executed PAPI command(s)
56 self.ret_code = ret_code
59 """Return string with human readable description of the group.
61 :returns: Readable description.
64 return ("papi_reply={papi_reply} "
67 "ret_code={ret_code}".
68 format(papi_reply=self.reply,
71 ret_code=self.ret_code))
74 """Return string executable as Python constructor call.
76 :returns: Executable constructor call.
79 return ("PapiResponse(papi_reply={papi_reply} "
82 "ret_code={ret_code})".
83 format(papi_reply=self.reply,
86 ret_code=self.ret_code))
89 class PapiExecutor(object):
90 """Contains methods for executing Python API commands on DUTs.
92 Use only with "with" statement, e.g.:
94 with PapiExecutor(node) as papi_exec:
95 papi_resp = papi_exec.add('show_version').execute_should_pass(err_msg)
98 def __init__(self, node):
101 :param node: Node to run command(s) on.
105 # Node to run command(s) on.
108 # The list of PAPI commands to be executed on the node.
109 self._api_command_list = list()
111 # The response on the PAPI commands.
112 self.response = PapiResponse()
118 self._ssh.connect(self._node)
120 raise RuntimeError("Cannot open SSH connection to host {host} to "
121 "execute PAPI command(s)".
122 format(host=self._node["host"]))
125 def __exit__(self, exc_type, exc_val, exc_tb):
126 self._ssh.disconnect(self._node)
129 """Empty the internal command list; return self.
131 Use when not sure whether previous usage has left something in the list.
133 :returns: self, so that method chaining is possible.
136 self._api_command_list = list()
139 def add(self, command, **kwargs):
140 """Add next command to internal command list; return self.
142 :param command: VPP API command.
143 :param kwargs: Optional key-value arguments.
146 :returns: self, so that method chaining is possible.
149 PapiHistory.add_to_papi_history(self._node, command, **kwargs)
150 self._api_command_list.append(dict(api_name=command, api_args=kwargs))
153 def execute(self, process_reply=True, ignore_errors=False, timeout=120):
154 """Turn internal command list into proper data and execute; return
157 This method also clears the internal command list.
159 :param process_reply: Process PAPI reply if True.
160 :param ignore_errors: If true, the errors in the reply are ignored.
161 :param timeout: Timeout in seconds.
162 :type process_reply: bool
163 :type ignore_errors: bool
165 :returns: Papi response including: papi reply, stdout, stderr and
168 :raises KeyError: If the reply is not correct.
171 local_list = self._api_command_list
173 # Clear first as execution may fail.
176 ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
180 json_data = json.loads(stdout)
181 for data in json_data:
183 api_reply_processed = dict(
184 api_name=data["api_name"],
185 api_reply=self._process_reply(data["api_reply"]))
191 papi_reply.append(api_reply_processed)
193 return PapiResponse(papi_reply=papi_reply,
198 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
199 process_reply=True, ignore_errors=False,
201 """Execute the PAPI commands and check the return code.
202 Raise exception if the PAPI command(s) failed.
204 Note: There are two exceptions raised to distinguish two situations. If
205 not needed, re-implement using only RuntimeError.
207 :param err_msg: The message used if the PAPI command(s) execution fails.
208 :param process_reply: Indicate whether or not to process PAPI reply.
209 :param ignore_errors: If true, the errors in the reply are ignored.
210 :param timeout: Timeout in seconds.
212 :type process_reply: bool
213 :type ignore_errors: bool
215 :returns: Papi response including: papi reply, stdout, stderr and
218 :raises RuntimeError: If no PAPI command(s) executed.
219 :raises AssertionError: If PAPI command(s) execution passed.
222 response = self.execute(process_reply=process_reply,
223 ignore_errors=ignore_errors,
226 if response.ret_code != 0:
227 raise AssertionError(err_msg)
230 def execute_should_fail(self,
231 err_msg="Execution of PAPI command did not fail.",
232 process_reply=False, ignore_errors=False,
234 """Execute the PAPI commands and check the return code.
235 Raise exception if the PAPI command(s) did not fail.
237 It does not return anything as we expect it fails.
239 Note: There are two exceptions raised to distinguish two situations. If
240 not needed, re-implement using only RuntimeError.
242 :param err_msg: The message used if the PAPI command(s) execution fails.
243 :param process_reply: Indicate whether or not to process PAPI reply.
244 :param ignore_errors: If true, the errors in the reply are ignored.
245 :param timeout: Timeout in seconds.
247 :type process_reply: bool
248 :type ignore_errors: bool
250 :raises RuntimeError: If no PAPI command(s) executed.
251 :raises AssertionError: If PAPI command(s) execution passed.
254 response = self.execute(process_reply=process_reply,
255 ignore_errors=ignore_errors,
258 if response.ret_code == 0:
259 raise AssertionError(err_msg)
262 def _process_api_data(api_d):
263 """Process API data for smooth converting to JSON string.
265 Apply binascii.hexlify() method for string values.
267 :param api_d: List of APIs with their arguments.
269 :returns: List of APIs with arguments pre-processed for JSON.
273 api_data_processed = list()
275 api_args_processed = dict()
276 for a_k, a_v in api["api_args"].iteritems():
277 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
278 api_args_processed[str(a_k)] = value
279 api_data_processed.append(dict(api_name=api["api_name"],
280 api_args=api_args_processed))
281 return api_data_processed
284 def _revert_api_reply(api_r):
285 """Process API reply / a part of API reply.
287 Apply binascii.unhexlify() method for unicode values.
289 TODO: Remove the disabled code when definitely not needed.
291 :param api_r: API reply.
293 :returns: Processed API reply / a part of API reply.
299 for reply_key, reply_v in api_r.iteritems():
300 for a_k, a_v in reply_v.iteritems():
301 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
303 # reply_value[a_k] = value
304 reply_value[a_k] = a_v
305 reply_dict[reply_key] = reply_value
308 def _process_reply(self, api_reply):
309 """Process API reply.
311 :param api_reply: API reply.
312 :type api_reply: dict or list of dict
313 :returns: Processed API reply.
317 if isinstance(api_reply, list):
318 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
320 reverted_reply = self._revert_api_reply(api_reply)
321 return reverted_reply
323 def _execute_papi(self, api_data, timeout=120):
324 """Execute PAPI command(s) on remote node and store the result.
326 :param api_data: List of APIs with their arguments.
327 :param timeout: Timeout in seconds.
330 :raises SSHTimeout: If PAPI command(s) execution has timed out.
331 :raises RuntimeError: If PAPI executor failed due to another reason.
335 RuntimeError("No API data provided.")
337 api_data_processed = self._process_api_data(api_data)
338 json_data = json.dumps(api_data_processed)
340 cmd = "python {fw_dir}/{papi_provider} --json_data '{json}'".format(
341 fw_dir=Constants.REMOTE_FW_DIR,
342 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
346 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
347 cmd=cmd, timeout=timeout)
349 logger.error("PAPI command(s) execution timeout on host {host}:"
350 "\n{apis}".format(host=self._node["host"],
354 raise RuntimeError("PAPI command(s) execution on host {host} "
355 "failed: {apis}".format(host=self._node["host"],
357 return ret_code, stdout, stderr