Papi: Rename methods argument
[csit.git] / resources / libraries / python / PapiExecutor.py
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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 """Python API executor library."""
15
16 import binascii
17 import json
18
19 from robot.api import logger
20
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
24
25 __all__ = ["PapiExecutor", "PapiResponse"]
26
27
28 class PapiResponse(object):
29     """Class for metadata specifying the Papi reply, stdout, stderr and return
30     code.
31     """
32
33     def __init__(self, papi_reply=None, stdout="", stderr="", ret_code=None):
34         """Construct the Papi response by setting the values needed.
35
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
41         :type stdout: str
42         :type stderr: str
43         :type ret_code: int
44         """
45
46         # API reply from last executed PAPI command(s)
47         self.reply = papi_reply
48
49         # stdout from last executed PAPI command(s)
50         self.stdout = stdout
51
52         # stderr from last executed PAPI command(s).
53         self.stderr = stderr
54
55         # return code from last executed PAPI command(s)
56         self.ret_code = ret_code
57
58     def __str__(self):
59         """Return string with human readable description of the group.
60
61         :returns: Readable description.
62         :rtype: str
63         """
64         return ("papi_reply={papi_reply} "
65                 "stdout={stdout} "
66                 "stderr={stderr} "
67                 "ret_code={ret_code}".
68                 format(papi_reply=self.reply,
69                        stdout=self.stdout,
70                        stderr=self.stderr,
71                        ret_code=self.ret_code))
72
73     def __repr__(self):
74         """Return string executable as Python constructor call.
75
76         :returns: Executable constructor call.
77         :rtype: str
78         """
79         return ("PapiResponse(papi_reply={papi_reply} "
80                 "stdout={stdout} "
81                 "stderr={stderr} "
82                 "ret_code={ret_code})".
83                 format(papi_reply=self.reply,
84                        stdout=self.stdout,
85                        stderr=self.stderr,
86                        ret_code=self.ret_code))
87
88
89 class PapiExecutor(object):
90     """Contains methods for executing Python API commands on DUTs.
91
92     Use only with "with" statement, e.g.:
93
94     with PapiExecutor(node) as papi_exec:
95         papi_resp = papi_exec.add('show_version').execute_should_pass(err_msg)
96     """
97
98     def __init__(self, node):
99         """Initialization.
100
101         :param node: Node to run command(s) on.
102         :type node: dict
103         """
104
105         # Node to run command(s) on.
106         self._node = node
107
108         # The list of PAPI commands to be executed on the node.
109         self._api_command_list = list()
110
111         # The response on the PAPI commands.
112         self.response = PapiResponse()
113
114         self._ssh = SSH()
115
116     def __enter__(self):
117         try:
118             self._ssh.connect(self._node)
119         except IOError:
120             raise RuntimeError("Cannot open SSH connection to host {host} to "
121                                "execute PAPI command(s)".
122                                format(host=self._node["host"]))
123         return self
124
125     def __exit__(self, exc_type, exc_val, exc_tb):
126         self._ssh.disconnect(self._node)
127
128     def clear(self):
129         """Empty the internal command list; return self.
130
131         Use when not sure whether previous usage has left something in the list.
132
133         :returns: self, so that method chaining is possible.
134         :rtype: PapiExecutor
135         """
136         self._api_command_list = list()
137         return self
138
139     def add(self, csit_papi_command, **kwargs):
140         """Add next command to internal command list; return self.
141
142         The argument name 'csit_papi_command' must be unique enough as it cannot
143         be repeated in kwargs.
144
145         :param csit_papi_command: VPP API command.
146         :param kwargs: Optional key-value arguments.
147         :type csit_papi_command: str
148         :type kwargs: dict
149         :returns: self, so that method chaining is possible.
150         :rtype: PapiExecutor
151         """
152         PapiHistory.add_to_papi_history(self._node, csit_papi_command, **kwargs)
153         self._api_command_list.append(dict(api_name=csit_papi_command,
154                                            api_args=kwargs))
155         return self
156
157     def execute(self, process_reply=True, ignore_errors=False, timeout=120):
158         """Turn internal command list into proper data and execute; return
159         PAPI response.
160
161         This method also clears the internal command list.
162
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
168         :type timeout: int
169         :returns: Papi response including: papi reply, stdout, stderr and
170             return code.
171         :rtype: PapiResponse
172         :raises KeyError: If the reply is not correct.
173         """
174
175         local_list = self._api_command_list
176
177         # Clear first as execution may fail.
178         self.clear()
179
180         ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
181
182         papi_reply = list()
183         if process_reply:
184             json_data = json.loads(stdout)
185             for data in json_data:
186                 try:
187                     api_reply_processed = dict(
188                         api_name=data["api_name"],
189                         api_reply=self._process_reply(data["api_reply"]))
190                 except KeyError:
191                     if ignore_errors:
192                         continue
193                     else:
194                         raise
195                 papi_reply.append(api_reply_processed)
196
197         return PapiResponse(papi_reply=papi_reply,
198                             stdout=stdout,
199                             stderr=stderr,
200                             ret_code=ret_code)
201
202     def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
203                             process_reply=True, ignore_errors=False,
204                             timeout=120):
205         """Execute the PAPI commands and check the return code.
206         Raise exception if the PAPI command(s) failed.
207
208         Note: There are two exceptions raised to distinguish two situations. If
209         not needed, re-implement using only RuntimeError.
210
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.
215         :type err_msg: str
216         :type process_reply: bool
217         :type ignore_errors: bool
218         :type timeout: int
219         :returns: Papi response including: papi reply, stdout, stderr and
220             return code.
221         :rtype: PapiResponse
222         :raises RuntimeError: If no PAPI command(s) executed.
223         :raises AssertionError: If PAPI command(s) execution passed.
224         """
225
226         response = self.execute(process_reply=process_reply,
227                                 ignore_errors=ignore_errors,
228                                 timeout=timeout)
229
230         if response.ret_code != 0:
231             raise AssertionError(err_msg)
232         return response
233
234     def execute_should_fail(self,
235                             err_msg="Execution of PAPI command did not fail.",
236                             process_reply=False, ignore_errors=False,
237                             timeout=120):
238         """Execute the PAPI commands and check the return code.
239         Raise exception if the PAPI command(s) did not fail.
240
241         It does not return anything as we expect it fails.
242
243         Note: There are two exceptions raised to distinguish two situations. If
244         not needed, re-implement using only RuntimeError.
245
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.
250         :type err_msg: str
251         :type process_reply: bool
252         :type ignore_errors: bool
253         :type timeout: int
254         :raises RuntimeError: If no PAPI command(s) executed.
255         :raises AssertionError: If PAPI command(s) execution passed.
256         """
257
258         response = self.execute(process_reply=process_reply,
259                                 ignore_errors=ignore_errors,
260                                 timeout=timeout)
261
262         if response.ret_code == 0:
263             raise AssertionError(err_msg)
264
265     @staticmethod
266     def _process_api_data(api_d):
267         """Process API data for smooth converting to JSON string.
268
269         Apply binascii.hexlify() method for string values.
270
271         :param api_d: List of APIs with their arguments.
272         :type api_d: list
273         :returns: List of APIs with arguments pre-processed for JSON.
274         :rtype: list
275         """
276
277         api_data_processed = list()
278         for api in api_d:
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
286
287     @staticmethod
288     def _revert_api_reply(api_r):
289         """Process API reply / a part of API reply.
290
291         Apply binascii.unhexlify() method for unicode values.
292
293         TODO: Remove the disabled code when definitely not needed.
294
295         :param api_r: API reply.
296         :type api_r: dict
297         :returns: Processed API reply / a part of API reply.
298         :rtype: dict
299         """
300
301         reply_dict = dict()
302         reply_value = dict()
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) \
306                 #     else a_v
307                 # reply_value[a_k] = value
308                 reply_value[a_k] = a_v
309             reply_dict[reply_key] = reply_value
310         return reply_dict
311
312     def _process_reply(self, api_reply):
313         """Process API reply.
314
315         :param api_reply: API reply.
316         :type api_reply: dict or list of dict
317         :returns: Processed API reply.
318         :rtype: list or dict
319         """
320
321         if isinstance(api_reply, list):
322             reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
323         else:
324             reverted_reply = self._revert_api_reply(api_reply)
325         return reverted_reply
326
327     def _execute_papi(self, api_data, timeout=120):
328         """Execute PAPI command(s) on remote node and store the result.
329
330         :param api_data: List of APIs with their arguments.
331         :param timeout: Timeout in seconds.
332         :type api_data: list
333         :type timeout: int
334         :raises SSHTimeout: If PAPI command(s) execution has timed out.
335         :raises RuntimeError: If PAPI executor failed due to another reason.
336         """
337
338         if not api_data:
339             RuntimeError("No API data provided.")
340
341         api_data_processed = self._process_api_data(api_data)
342         json_data = json.dumps(api_data_processed)
343
344         cmd = "python {fw_dir}/{papi_provider} --json_data '{json}'".format(
345             fw_dir=Constants.REMOTE_FW_DIR,
346             papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
347             json=json_data)
348
349         try:
350             ret_code, stdout, stderr = self._ssh.exec_command_sudo(
351                 cmd=cmd, timeout=timeout)
352         except SSHTimeout:
353             logger.error("PAPI command(s) execution timeout on host {host}:"
354                          "\n{apis}".format(host=self._node["host"],
355                                            apis=api_data))
356             raise
357         except Exception:
358             raise RuntimeError("PAPI command(s) execution on host {host} "
359                                "failed: {apis}".format(host=self._node["host"],
360                                                        apis=api_data))
361         return ret_code, stdout, stderr