CSIT-1451: PapiHistory
[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, command, **kwargs):
140         """Add next command to internal command list; return self.
141
142         :param command: VPP API command.
143         :param kwargs: Optional key-value arguments.
144         :type command: str
145         :type kwargs: dict
146         :returns: self, so that method chaining is possible.
147         :rtype: PapiExecutor
148         """
149         PapiHistory.add_to_papi_history(self._node, command, **kwargs)
150         self._api_command_list.append(dict(api_name=command, api_args=kwargs))
151         return self
152
153     def execute(self, process_reply=True, ignore_errors=False, timeout=120):
154         """Turn internal command list into proper data and execute; return
155         PAPI response.
156
157         This method also clears the internal command list.
158
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
164         :type timeout: int
165         :returns: Papi response including: papi reply, stdout, stderr and
166             return code.
167         :rtype: PapiResponse
168         :raises KeyError: If the reply is not correct.
169         """
170
171         local_list = self._api_command_list
172
173         # Clear first as execution may fail.
174         self.clear()
175
176         ret_code, stdout, stderr = self._execute_papi(local_list, timeout)
177
178         papi_reply = list()
179         if process_reply:
180             json_data = json.loads(stdout)
181             for data in json_data:
182                 try:
183                     api_reply_processed = dict(
184                         api_name=data["api_name"],
185                         api_reply=self._process_reply(data["api_reply"]))
186                 except KeyError:
187                     if ignore_errors:
188                         continue
189                     else:
190                         raise
191                 papi_reply.append(api_reply_processed)
192
193         return PapiResponse(papi_reply=papi_reply,
194                             stdout=stdout,
195                             stderr=stderr,
196                             ret_code=ret_code)
197
198     def execute_should_pass(self, err_msg="Failed to execute PAPI command.",
199                             process_reply=True, ignore_errors=False,
200                             timeout=120):
201         """Execute the PAPI commands and check the return code.
202         Raise exception if the PAPI command(s) failed.
203
204         Note: There are two exceptions raised to distinguish two situations. If
205         not needed, re-implement using only RuntimeError.
206
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.
211         :type err_msg: str
212         :type process_reply: bool
213         :type ignore_errors: bool
214         :type timeout: int
215         :returns: Papi response including: papi reply, stdout, stderr and
216             return code.
217         :rtype: PapiResponse
218         :raises RuntimeError: If no PAPI command(s) executed.
219         :raises AssertionError: If PAPI command(s) execution passed.
220         """
221
222         response = self.execute(process_reply=process_reply,
223                                 ignore_errors=ignore_errors,
224                                 timeout=timeout)
225
226         if response.ret_code != 0:
227             raise AssertionError(err_msg)
228         return response
229
230     def execute_should_fail(self,
231                             err_msg="Execution of PAPI command did not fail.",
232                             process_reply=False, ignore_errors=False,
233                             timeout=120):
234         """Execute the PAPI commands and check the return code.
235         Raise exception if the PAPI command(s) did not fail.
236
237         It does not return anything as we expect it fails.
238
239         Note: There are two exceptions raised to distinguish two situations. If
240         not needed, re-implement using only RuntimeError.
241
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.
246         :type err_msg: str
247         :type process_reply: bool
248         :type ignore_errors: bool
249         :type timeout: int
250         :raises RuntimeError: If no PAPI command(s) executed.
251         :raises AssertionError: If PAPI command(s) execution passed.
252         """
253
254         response = self.execute(process_reply=process_reply,
255                                 ignore_errors=ignore_errors,
256                                 timeout=timeout)
257
258         if response.ret_code == 0:
259             raise AssertionError(err_msg)
260
261     @staticmethod
262     def _process_api_data(api_d):
263         """Process API data for smooth converting to JSON string.
264
265         Apply binascii.hexlify() method for string values.
266
267         :param api_d: List of APIs with their arguments.
268         :type api_d: list
269         :returns: List of APIs with arguments pre-processed for JSON.
270         :rtype: list
271         """
272
273         api_data_processed = list()
274         for api in api_d:
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
282
283     @staticmethod
284     def _revert_api_reply(api_r):
285         """Process API reply / a part of API reply.
286
287         Apply binascii.unhexlify() method for unicode values.
288
289         TODO: Remove the disabled code when definitely not needed.
290
291         :param api_r: API reply.
292         :type api_r: dict
293         :returns: Processed API reply / a part of API reply.
294         :rtype: dict
295         """
296
297         reply_dict = dict()
298         reply_value = dict()
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) \
302                 #     else a_v
303                 # reply_value[a_k] = value
304                 reply_value[a_k] = a_v
305             reply_dict[reply_key] = reply_value
306         return reply_dict
307
308     def _process_reply(self, api_reply):
309         """Process API reply.
310
311         :param api_reply: API reply.
312         :type api_reply: dict or list of dict
313         :returns: Processed API reply.
314         :rtype: list or dict
315         """
316
317         if isinstance(api_reply, list):
318             reverted_reply = [self._revert_api_reply(a_r) for a_r in api_reply]
319         else:
320             reverted_reply = self._revert_api_reply(api_reply)
321         return reverted_reply
322
323     def _execute_papi(self, api_data, timeout=120):
324         """Execute PAPI command(s) on remote node and store the result.
325
326         :param api_data: List of APIs with their arguments.
327         :param timeout: Timeout in seconds.
328         :type api_data: list
329         :type timeout: int
330         :raises SSHTimeout: If PAPI command(s) execution has timed out.
331         :raises RuntimeError: If PAPI executor failed due to another reason.
332         """
333
334         if not api_data:
335             RuntimeError("No API data provided.")
336
337         api_data_processed = self._process_api_data(api_data)
338         json_data = json.dumps(api_data_processed)
339
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,
343             json=json_data)
344
345         try:
346             ret_code, stdout, stderr = self._ssh.exec_command_sudo(
347                 cmd=cmd, timeout=timeout)
348         except SSHTimeout:
349             logger.error("PAPI command(s) execution timeout on host {host}:"
350                          "\n{apis}".format(host=self._node["host"],
351                                            apis=api_data))
352             raise
353         except Exception:
354             raise RuntimeError("PAPI command(s) execution on host {host} "
355                                "failed: {apis}".format(host=self._node["host"],
356                                                        apis=api_data))
357         return ret_code, stdout, stderr