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
24 __all__ = ["PapiExecutor", "PapiResponse"]
26 # TODO: Implement Papi History
27 # from resources.libraries.python.PapiHistory import PapiHistory
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):
36 """Construct the Papi response by setting the values needed.
38 :param papi_reply: API reply from last executed PAPI command(s).
39 :param stdout: stdout from last executed PAPI command(s).
40 :param stderr: stderr from last executed PAPI command(s).
41 :param ret_code: ret_code from last executed PAPI command(s).
42 :type papi_reply: list
48 # API reply from last executed PAPI command(s)
49 self.reply = papi_reply
51 # stdout from last executed PAPI command(s)
54 # stderr from last executed PAPI command(s).
57 # return code from last executed PAPI command(s)
58 self.ret_code = ret_code
61 """Return string with human readable description of the group.
63 :returns: Readable description.
66 return ("papi_reply={papi_reply} "
69 "ret_code={ret_code}".
70 format(papi_reply=self.reply,
73 ret_code=self.ret_code))
76 """Return string executable as Python constructor call.
78 :returns: Executable constructor call.
81 return ("PapiResponse(papi_reply={papi_reply} "
84 "ret_code={ret_code})".
85 format(papi_reply=self.reply,
88 ret_code=self.ret_code))
91 class PapiExecutor(object):
92 """Contains methods for executing Python API commands on DUTs.
94 Use only with "with" statement, e.g.:
96 with PapiExecutor(node) as papi_exec:
97 papi_resp = papi_exec.add('show_version').execute_should_pass(err_msg)
100 def __init__(self, node):
103 :param node: Node to run command(s) on.
107 # Node to run command(s) on.
110 # The list of PAPI commands to be executed on the node.
111 self._api_command_list = list()
113 # The response on the PAPI commands.
114 self.response = PapiResponse()
120 self._ssh.connect(self._node)
122 raise RuntimeError("Cannot open SSH connection to host {host} to "
123 "execute PAPI command(s)".
124 format(host=self._node["host"]))
127 def __exit__(self, exc_type, exc_val, exc_tb):
128 self._ssh.disconnect(self._node)
131 """Empty the internal command list; return self.
133 Use when not sure whether previous usage has left something in the list.
135 :returns: self, so that method chaining is possible.
138 self._api_command_list = list()
141 def add(self, command, **kwargs):
142 """Add next command to internal command list; return self.
144 :param command: VPP API command.
145 :param kwargs: Optional key-value arguments.
148 :returns: self, so that method chaining is possible.
151 self._api_command_list.append(dict(api_name=command, api_args=kwargs))
154 def execute(self, process_reply=True, ignore_errors=False, timeout=120):
155 """Turn internal command list into proper data and execute; return
158 This method also clears the internal command list.
160 :param process_reply: Process PAPI reply if True.
161 :param ignore_errors: If true, the errors in the reply are ignored.
162 :param timeout: Timeout in seconds.
163 :type process_reply: bool
164 :type ignore_errors: bool
166 :returns: Papi response including: papi reply, stdout, stderr and
169 :raises KeyError: If the reply is not correct.
172 local_list = self._api_command_list
174 # Clear first as execution may fail.
177 ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
181 json_data = json.loads(stdout)
182 for data in json_data:
184 api_reply_processed = dict(
185 api_name=data["api_name"],
186 api_reply=self._process_reply(data["api_reply"]))
192 papi_reply.append(api_reply_processed)
194 return PapiResponse(papi_reply=papi_reply,
199 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
200 process_reply=True, ignore_errors=False,
202 """Execute the PAPI commands and check the return code.
203 Raise exception if the PAPI command(s) failed.
205 Note: There are two exceptions raised to distinguish two situations. If
206 not needed, re-implement using only RuntimeError.
208 :param err_msg: The message used if the PAPI command(s) execution fails.
209 :param process_reply: Indicate whether or not to process PAPI reply.
210 :param ignore_errors: If true, the errors in the reply are ignored.
211 :param timeout: Timeout in seconds.
213 :type process_reply: bool
214 :type ignore_errors: bool
216 :returns: Papi response including: papi reply, stdout, stderr and
219 :raises RuntimeError: If no PAPI command(s) executed.
220 :raises AssertionError: If PAPI command(s) execution passed.
223 response = self.execute(process_reply=process_reply,
224 ignore_errors=ignore_errors,
227 if response.ret_code != 0:
228 raise AssertionError(err_msg)
231 def execute_should_fail(self,
232 err_msg="Execution of PAPI command did not fail.",
233 process_reply=False, ignore_errors=False,
235 """Execute the PAPI commands and check the return code.
236 Raise exception if the PAPI command(s) did not fail.
238 It does not return anything as we expect it fails.
240 Note: There are two exceptions raised to distinguish two situations. If
241 not needed, re-implement using only RuntimeError.
243 :param err_msg: The message used if the PAPI command(s) execution fails.
244 :param process_reply: Indicate whether or not to process PAPI reply.
245 :param ignore_errors: If true, the errors in the reply are ignored.
246 :param timeout: Timeout in seconds.
248 :type process_reply: bool
249 :type ignore_errors: bool
251 :raises RuntimeError: If no PAPI command(s) executed.
252 :raises AssertionError: If PAPI command(s) execution passed.
255 response = self.execute(process_reply=process_reply,
256 ignore_errors=ignore_errors,
259 if response.ret_code == 0:
260 raise AssertionError(err_msg)
263 def _process_api_data(api_d):
264 """Process API data for smooth converting to JSON string.
266 Apply binascii.hexlify() method for string values.
268 :param api_d: List of APIs with their arguments.
270 :returns: List of APIs with arguments pre-processed for JSON.
274 api_data_processed = list()
276 api_args_processed = dict()
277 for a_k, a_v in api["api_args"].iteritems():
278 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
279 api_args_processed[str(a_k)] = value
280 api_data_processed.append(dict(api_name=api["api_name"],
281 api_args=api_args_processed))
282 return api_data_processed
285 def _revert_api_reply(api_r):
286 """Process API reply / a part of API reply.
288 Apply binascii.unhexlify() method for unicode values.
290 TODO: Remove the disabled code when definitely not needed.
292 :param api_r: API reply.
294 :returns: Processed API reply / a part of API reply.
300 for reply_key, reply_v in api_r.iteritems():
301 for a_k, a_v in reply_v.iteritems():
302 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
304 # reply_value[a_k] = value
305 reply_value[a_k] = a_v
306 reply_dict[reply_key] = reply_value
309 def _process_reply(self, api_reply):
310 """Process API reply.
312 :param api_reply: API reply.
313 :type api_reply: dict or list of dict
314 :returns: Processed API reply.
318 if isinstance(api_reply, list):
319 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
321 reverted_reply = self._revert_api_reply(api_reply)
322 return reverted_reply
324 def _execute_papi(self, api_data, timeout=120):
325 """Execute PAPI command(s) on remote node and store the result.
327 :param api_data: List of APIs with their arguments.
328 :param timeout: Timeout in seconds.
331 :raises SSHTimeout: If PAPI command(s) execution has timed out.
332 :raises RuntimeError: If PAPI executor failed due to another reason.
336 RuntimeError("No API data provided.")
338 api_data_processed = self._process_api_data(api_data)
339 json_data = json.dumps(api_data_processed)
341 cmd = "python {fw_dir}/{papi_provider} --json_data '{json}'".format(
342 fw_dir=Constants.REMOTE_FW_DIR,
343 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
347 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
348 cmd=cmd, timeout=timeout)
350 logger.error("PAPI command(s) execution timeout on host {host}:"
351 "\n{apis}".format(host=self._node["host"],
355 raise RuntimeError("PAPI command(s) execution on host {host} "
356 "failed: {apis}".format(host=self._node["host"],
358 return ret_code, stdout, stderr