FIX: Update PAPI to work with string changes in Python API
[csit.git] / resources / libraries / python / PapiExecutor.py
1 # Copyright (c) 2018 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 paramiko.ssh_exception import SSHException
20 from robot.api import logger
21
22 from resources.libraries.python.constants import Constants
23 from resources.libraries.python.PapiErrors import PapiInitError, \
24     PapiJsonFileError, PapiCommandError, PapiCommandInputError
25 # TODO: from resources.libraries.python.PapiHistory import PapiHistory
26 from resources.libraries.python.ssh import SSH, SSHTimeout
27
28 __all__ = ['PapiExecutor']
29
30
31 class PapiExecutor(object):
32     """Contains methods for executing Python API commands on DUTs."""
33
34     def __init__(self, node):
35         self._stdout = None
36         self._stderr = None
37         self._ret_code = None
38         self._node = node
39         self._json_data = None
40         self._api_reply = list()
41         self._api_data = None
42
43         self._ssh = SSH()
44         try:
45             self._ssh.connect(node)
46         except:
47             raise SSHException('Cannot open SSH connection to host {host} to '
48                                'execute PAPI command(s)'.
49                                format(host=self._node['host']))
50
51     def __enter__(self):
52         return self
53
54     def __exit__(self, exc_type, exc_val, exc_tb):
55         pass
56
57     @staticmethod
58     def _process_api_data(api_d):
59         """Process API data for smooth converting to JSON string.
60
61         Apply binascii.hexlify() method for string values.
62
63         :param api_d: List of APIs with their arguments.
64         :type api_d: list
65         :returns: List of APIs with arguments pre-processed for JSON.
66         :rtype: list
67         """
68
69         api_data_processed = list()
70         for api in api_d:
71             api_name = api['api_name']
72             api_args = api['api_args']
73             api_processed = dict(api_name=api_name)
74             api_args_processed = dict()
75             for a_k, a_v in api_args.iteritems():
76                 value = binascii.hexlify(a_v) if isinstance(a_v, str) else a_v
77                 api_args_processed[str(a_k)] = value
78             api_processed['api_args'] = api_args_processed
79             api_data_processed.append(api_processed)
80         return api_data_processed
81
82     @staticmethod
83     def _revert_api_reply(api_r):
84         """Process API reply / a part of API reply.
85
86         Apply binascii.unhexlify() method for unicode values.
87
88         :param api_r: API reply.
89         :type api_r: dict
90         :returns: Processed API reply / a part of API reply.
91         :rtype: dict
92         """
93
94         reply_dict = dict()
95         reply_value = dict()
96         for reply_key, reply_v in api_r.iteritems():
97             for a_k, a_v in reply_v.iteritems():
98                 # value = binascii.unhexlify(a_v) if isinstance(a_v, unicode) \
99                 #     else a_v
100                 # reply_value[a_k] = value
101                 reply_value[a_k] = a_v
102             reply_dict[reply_key] = reply_value
103         return reply_dict
104
105     def _process_reply(self, api_reply):
106         """Process API reply.
107
108         :param api_reply: API reply.
109         :type api_reply: dict or list of dict
110         :returns: Processed API reply.
111         :rtype: list or dict
112         """
113
114         if isinstance(api_reply, list):
115             reverted_reply = list()
116             for a_r in api_reply:
117                 reverted_reply.append(self._revert_api_reply(a_r))
118         else:
119             reverted_reply = self._revert_api_reply(api_reply)
120         return reverted_reply
121
122     def _process_json_data(self):
123         """Process received JSON data."""
124
125         for data in self._json_data:
126             api_name = data['api_name']
127             api_reply = data['api_reply']
128             api_reply_processed = dict(
129                 api_name=api_name, api_reply=self._process_reply(api_reply))
130             self._api_reply.append(api_reply_processed)
131
132     def execute_papi(self, api_data, timeout=120):
133         """Execute PAPI command(s) on remote node and store the result.
134
135         :param api_data: List of APIs with their arguments.
136         :param timeout: Timeout in seconds.
137         :type api_data: list
138         :type timeout: int
139         :raises SSHTimeout: If PAPI command(s) execution is timed out.
140         :raises PapiInitError: If PAPI initialization failed.
141         :raises PapiJsonFileError: If no api.json file found.
142         :raises PapiCommandError: If PAPI command(s) execution failed.
143         :raises PapiCommandInputError: If invalid attribute name or invalid
144             value is used in API call.
145         :raises RuntimeError: If PAPI executor failed due to another reason.
146         """
147         self._api_data = api_data
148         api_data_processed = self._process_api_data(api_data)
149         json_data = json.dumps(api_data_processed)
150
151         cmd = "python {fw_dir}/{papi_provider} --json_data '{json}'".format(
152             fw_dir=Constants.REMOTE_FW_DIR,
153             papi_provider=Constants.RESOURCES_PAPI_PROVIDER,
154             json=json_data)
155
156         try:
157             ret_code, stdout, stderr = self._ssh.exec_command_sudo(
158                 cmd=cmd, timeout=timeout)
159         except SSHTimeout:
160             logger.error('PAPI command(s) execution timeout on host {host}:'
161                          '\n{apis}'.format(host=self._node['host'],
162                                            apis=self._api_data))
163             raise
164         except (PapiInitError, PapiJsonFileError, PapiCommandError,
165                 PapiCommandInputError):
166             logger.error('PAPI command(s) execution failed on host {host}'.
167                          format(host=self._node['host']))
168             raise
169         except:
170             raise RuntimeError('PAPI command(s) execution on host {host} '
171                                'failed: {apis}'.format(host=self._node['host'],
172                                                        apis=self._api_data))
173
174         self._ret_code = ret_code
175         self._stdout = stdout
176         self._stderr = stderr
177
178     def papi_should_have_failed(self):
179         """Read return code from last executed script and raise exception if the
180         PAPI command(s) didn't fail.
181
182         :raises RuntimeError: When no PAPI command executed.
183         :raises AssertionError: If PAPI command(s) execution passed.
184         """
185
186         if self._ret_code is None:
187             raise RuntimeError("First execute the PAPI command(s)!")
188         if self._ret_code == 0:
189             raise AssertionError(
190                 "PAPI command(s) execution passed, but failure was expected: "
191                 "{apis}".format(apis=self._api_data))
192
193     def papi_should_have_passed(self):
194         """Read return code from last executed script and raise exception if the
195         PAPI command(s) failed.
196
197         :raises RuntimeError: When no PAPI command executed.
198         :raises AssertionError: If PAPI command(s) execution failed.
199         """
200
201         if self._ret_code is None:
202             raise RuntimeError("First execute the PAPI command(s)!")
203         if self._ret_code != 0:
204             raise AssertionError(
205                 "PAPI command(s) execution failed, but success was expected: "
206                 "{apis}".format(apis=self._api_data))
207
208     def get_papi_stdout(self):
209         """Returns value of stdout from last executed PAPI command(s)."""
210
211         return self._stdout
212
213     def get_papi_stderr(self):
214         """Returns value of stderr from last executed PAPI command(s)."""
215
216         return self._stderr
217
218     def get_papi_reply(self):
219         """Returns api reply from last executed PAPI command(s)."""
220
221         self._json_data = json.loads(self._stdout)
222         self._process_json_data()
223
224         return self._api_reply