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, csit_papi_command, **kwargs):
140 """Add next command to internal command list; return self.
142 The argument name 'csit_papi_command' must be unique enough as it cannot
143 be repeated in kwargs.
145 :param csit_papi_command: VPP API command.
146 :param kwargs: Optional key-value arguments.
147 :type csit_papi_command: str
149 :returns: self, so that method chaining is possible.
152 PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
153 self._api_command_list.append(dict(api_name=csit_papi_command,
157 def execute(self, process_reply=True, ignore_errors=False, timeout=120):
158 """Turn internal command list into proper data and execute; return
161 This method also clears the internal command list.
163 :param process_reply: Process PAPI reply if True.
164 :param ignore_errors: If true, the errors in the reply are ignored.
165 :param timeout: Timeout in seconds.
166 :type process_reply: bool
167 :type ignore_errors: bool
169 :returns: Papi response including: papi reply, stdout, stderr and
172 :raises KeyError: If the reply is not correct.
175 local_list = self._api_command_list
177 # Clear first as execution may fail.
180 ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
184 json_data = json.loads(stdout)
185 for data in json_data:
187 api_reply_processed = dict(
188 api_name=data["api_name"],
189 api_reply=self._process_reply(data["api_reply"]))
195 papi_reply.append(api_reply_processed)
197 return PapiResponse(papi_reply=papi_reply,
202 def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
203 process_reply=True, ignore_errors=False,
205 """Execute the PAPI commands and check the return code.
206 Raise exception if the PAPI command(s) failed.
208 Note: There are two exceptions raised to distinguish two situations. If
209 not needed, re-implement using only RuntimeError.
211 :param err_msg: The message used if the PAPI command(s) execution fails.
212 :param process_reply: Indicate whether or not to process PAPI reply.
213 :param ignore_errors: If true, the errors in the reply are ignored.
214 :param timeout: Timeout in seconds.
216 :type process_reply: bool
217 :type ignore_errors: bool
219 :returns: Papi response including: papi reply, stdout, stderr and
222 :raises RuntimeError: If no PAPI command(s) executed.
223 :raises AssertionError: If PAPI command(s) execution passed.
226 response = self.execute(process_reply=process_reply,
227 ignore_errors=ignore_errors,
230 if response.ret_code != 0:
231 raise AssertionError(err_msg)
234 def execute_should_fail(self,
235 err_msg="Execution of PAPI command did not fail.",
236 process_reply=False, ignore_errors=False,
238 """Execute the PAPI commands and check the return code.
239 Raise exception if the PAPI command(s) did not fail.
241 It does not return anything as we expect it fails.
243 Note: There are two exceptions raised to distinguish two situations. If
244 not needed, re-implement using only RuntimeError.
246 :param err_msg: The message used if the PAPI command(s) execution fails.
247 :param process_reply: Indicate whether or not to process PAPI reply.
248 :param ignore_errors: If true, the errors in the reply are ignored.
249 :param timeout: Timeout in seconds.
251 :type process_reply: bool
252 :type ignore_errors: bool
254 :raises RuntimeError: If no PAPI command(s) executed.
255 :raises AssertionError: If PAPI command(s) execution passed.
258 response = self.execute(process_reply=process_reply,
259 ignore_errors=ignore_errors,
262 if response.ret_code == 0:
263 raise AssertionError(err_msg)
266 def _process_api_data(api_d):
267 """Process API data for smooth converting to JSON string.
269 Apply binascii.hexlify() method for string values.
271 :param api_d: List of APIs with their arguments.
273 :returns: List of APIs with arguments pre-processed for JSON.
277 api_data_processed = list()
279 api_args_processed = dict()
280 for a_k, a_v in api["api_args"].iteritems():
281 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
282 api_args_processed[str(a_k)] = value
283 api_data_processed.append(dict(api_name=api["api_name"],
284 api_args=api_args_processed))
285 return api_data_processed
288 def _revert_api_reply(api_r):
289 """Process API reply / a part of API reply.
291 Apply binascii.unhexlify() method for unicode values.
293 TODO: Remove the disabled code when definitely not needed.
295 :param api_r: API reply.
297 :returns: Processed API reply / a part of API reply.
303 for reply_key, reply_v in api_r.iteritems():
304 for a_k, a_v in reply_v.iteritems():
305 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
307 # reply_value[a_k] = value
308 reply_value[a_k] = a_v
309 reply_dict[reply_key] = reply_value
312 def _process_reply(self, api_reply):
313 """Process API reply.
315 :param api_reply: API reply.
316 :type api_reply: dict or list of dict
317 :returns: Processed API reply.
321 if isinstance(api_reply, list):
322 reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
324 reverted_reply = self._revert_api_reply(api_reply)
325 return reverted_reply
327 def _execute_papi(self, api_data, timeout=120):
328 """Execute PAPI command(s) on remote node and store the result.
330 :param api_data: List of APIs with their arguments.
331 :param timeout: Timeout in seconds.
334 :raises SSHTimeout: If PAPI command(s) execution has timed out.
335 :raises RuntimeError: If PAPI executor failed due to another reason.
339 RuntimeError("No API data provided.")
341 api_data_processed = self._process_api_data(api_data)
342 json_data = json.dumps(api_data_processed)
344 cmd = "{fw_dir}/{papi_provider} --json_data '{json}'".format(
345 fw_dir=Constants.REMOTE_FW_DIR,
346 papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
350 ret_code, stdout, stderr = self._ssh.exec_command_sudo(
351 cmd=cmd, timeout=timeout)
353 logger.error("PAPI command(s) execution timeout on host {host}:"
354 "\n{apis}".format(host=self._node["host"],
358 raise RuntimeError("PAPI command(s) execution on host {host} "
359 "failed: {apis}".format(host=self._node["host"],
361 return ret_code, stdout, stderr