PAPI: Fix PyLint errors
[csit.git] / resources / libraries / python / VatExecutor.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 """VAT executor library."""
15
16 import json
17 from os import remove
18
19 from paramiko.ssh_exception import SSHException
20 from robot.api import logger
21
22 from resources.libraries.python.ssh import SSH, SSHTimeout
23 from resources.libraries.python.Constants import Constants
24 from resources.libraries.python.PapiHistory import PapiHistory
25
26 __all__ = ['VatExecutor']
27
28
29 def cleanup_vat_json_output(json_output, vat_name=None):
30     """Return VAT JSON output cleaned from VAT clutter.
31
32     Clean up VAT JSON output from clutter like vat# prompts and such.
33
34     :param json_output: Cluttered JSON output.
35     :param vat_name: Name of the VAT script.
36     :type json_output: JSON
37     :type vat_name: str
38     :returns: Cleaned up output JSON string.
39     :rtype: JSON
40     """
41
42     retval = json_output
43     clutter = ['vat#', 'dump_interface_table error: Misc']
44     if vat_name:
45         remote_file_path = '{0}/{1}/{2}'.format(Constants.REMOTE_FW_DIR,
46                                                 Constants.RESOURCES_TPL_VAT,
47                                                 vat_name)
48         clutter.append("{0}(2):".format(remote_file_path))
49     for garbage in clutter:
50         retval = retval.replace(garbage, '')
51     return retval
52
53
54 def get_vpp_pid(node):
55     """Get PID of running VPP process.
56
57     :param node: DUT node.
58     :type node: dict
59     :returns: PID of VPP process / List of PIDs if more VPP processes are
60         running on the DUT node.
61     :rtype: int or list
62     """
63     import resources.libraries.python.DUTSetup as PidLib
64     pid = PidLib.DUTSetup.get_vpp_pid(node)
65     return pid
66
67
68 class VatExecutor(object):
69     """Contains methods for executing VAT commands on DUTs."""
70     def __init__(self):
71         self._stdout = None
72         self._stderr = None
73         self._ret_code = None
74         self._script_name = None
75
76     def execute_script(self, vat_name, node, timeout=120, json_out=True,
77                        copy_on_execute=False):
78         """Execute VAT script on remote node, and store the result. There is an
79         option to copy script from local host to remote host before execution.
80         Path is defined automatically.
81
82         :param vat_name: Name of the vat script file. Only the file name of
83             the script is required, the resources path is prepended
84             automatically.
85         :param node: Node to execute the VAT script on.
86         :param timeout: Seconds to allow the script to run.
87         :param json_out: Require JSON output.
88         :param copy_on_execute: If true, copy the file from local host to remote
89             before executing.
90         :type vat_name: str
91         :type node: dict
92         :type timeout: int
93         :type json_out: bool
94         :type copy_on_execute: bool
95         :raises SSHException: If cannot open connection for VAT.
96         :raises SSHTimeout: If VAT execution is timed out.
97         :raises RuntimeError: If VAT script execution fails.
98         """
99         ssh = SSH()
100         try:
101             ssh.connect(node)
102         except:
103             raise SSHException("Cannot open SSH connection to execute VAT "
104                                "command(s) from vat script {name}"
105                                .format(name=vat_name))
106
107         if copy_on_execute:
108             ssh.scp(vat_name, vat_name)
109             remote_file_path = vat_name
110             with open(vat_name, 'r') as vat_file:
111                 for line in vat_file:
112                     PapiHistory.add_to_papi_history(node,
113                                                     line.replace('\n', ''),
114                                                     papi=False)
115         else:
116             remote_file_path = '{0}/{1}/{2}'.format(Constants.REMOTE_FW_DIR,
117                                                     Constants.RESOURCES_TPL_VAT,
118                                                     vat_name)
119
120         cmd = "{vat_bin} {json} in {vat_path} script".format(
121             vat_bin=Constants.VAT_BIN_NAME,
122             json="json" if json_out is True else "",
123             vat_path=remote_file_path)
124
125         try:
126             ret_code, stdout, stderr = ssh.exec_command_sudo(cmd=cmd,
127                                                              timeout=timeout)
128         except SSHTimeout:
129             logger.error("VAT script execution timeout: {0}".format(cmd))
130             raise
131         except:
132             raise RuntimeError("VAT script execution failed: {0}".format(cmd))
133
134         self._ret_code = ret_code
135         self._stdout = stdout
136         self._stderr = stderr
137         self._script_name = vat_name
138
139     def write_and_execute_script(self, node, tmp_fn, commands, timeout=300,
140                                  json_out=False):
141         """Write VAT commands to the script, copy it to node and execute it.
142
143         :param node: VPP node.
144         :param tmp_fn: Path to temporary file script.
145         :param commands: VAT command list.
146         :param timeout: Seconds to allow the script to run.
147         :param json_out: Require JSON output.
148         :type node: dict
149         :type tmp_fn: str
150         :type commands: list
151         :type timeout: int
152         :type json_out: bool
153         """
154         with open(tmp_fn, 'w') as tmp_f:
155             tmp_f.writelines(commands)
156
157         self.execute_script(tmp_fn, node, timeout=timeout, json_out=json_out,
158                             copy_on_execute=True)
159         remove(tmp_fn)
160
161     def execute_script_json_out(self, vat_name, node, timeout=120):
162         """Pass all arguments to 'execute_script' method, then cleanup returned
163         json output.
164
165         :param vat_name: Name of the vat script file. Only the file name of
166             the script is required, the resources path is prepended
167             automatically.
168         :param node: Node to execute the VAT script on.
169         :param timeout: Seconds to allow the script to run.
170         :type vat_name: str
171         :type node: dict
172         :type timeout: int
173         """
174         self.execute_script(vat_name, node, timeout, json_out=True)
175         self._stdout = cleanup_vat_json_output(self._stdout, vat_name=vat_name)
176
177     def script_should_have_failed(self):
178         """Read return code from last executed script and raise exception if the
179         script didn't fail."""
180         if self._ret_code is None:
181             raise Exception("First execute the script!")
182         if self._ret_code == 0:
183             raise AssertionError(
184                 "VAT Script execution passed, but failure was expected: {cmd}"
185                 .format(cmd=self._script_name))
186
187     def script_should_have_passed(self):
188         """Read return code from last executed script and raise exception if the
189         script failed."""
190         if self._ret_code is None:
191             raise Exception("First execute the script!")
192         if self._ret_code != 0:
193             raise AssertionError(
194                 "VAT Script execution failed, but success was expected: {cmd}"
195                 .format(cmd=self._script_name))
196
197     def get_script_stdout(self):
198         """Returns value of stdout from last executed script."""
199         return self._stdout
200
201     def get_script_stderr(self):
202         """Returns value of stderr from last executed script."""
203         return self._stderr
204
205     @staticmethod
206     def cmd_from_template(node, vat_template_file, json_param=True, **vat_args):
207         """Execute VAT script on specified node. This method supports
208         script templates with parameters.
209
210         :param node: Node in topology on witch the script is executed.
211         :param vat_template_file: Template file of VAT script.
212         :param vat_args: Arguments to the template file.
213         :returns: List of JSON objects returned by VAT.
214         """
215         with VatTerminal(node, json_param=json_param) as vat:
216             return vat.vat_terminal_exec_cmd_from_template(vat_template_file,
217                                                            **vat_args)
218
219
220 class VatTerminal(object):
221     """VAT interactive terminal.
222
223     :param node: Node to open VAT terminal on.
224     :param json_param: Defines if outputs from VAT are in JSON format.
225         Default is True.
226     :type node: dict
227     :type json_param: bool
228
229     """
230
231     __VAT_PROMPT = ("vat# ", )
232     __LINUX_PROMPT = (":~# ", ":~$ ", "~]$ ", "~]# ")
233
234     def __init__(self, node, json_param=True):
235         json_text = ' json' if json_param else ''
236         self.json = json_param
237         self._node = node
238         self._ssh = SSH()
239         self._ssh.connect(self._node)
240         try:
241             self._tty = self._ssh.interactive_terminal_open()
242         except Exception:
243             raise RuntimeError("Cannot open interactive terminal on node {0}".
244                                format(self._node))
245
246         for _ in range(3):
247             try:
248                 self._ssh.interactive_terminal_exec_command(
249                     self._tty,
250                     'sudo -S {0}{1}'.format(Constants.VAT_BIN_NAME, json_text),
251                     self.__VAT_PROMPT)
252             except Exception:
253                 continue
254             else:
255                 break
256         else:
257             vpp_pid = get_vpp_pid(self._node)
258             if vpp_pid:
259                 if isinstance(vpp_pid, int):
260                     logger.trace("VPP running on node {0}".
261                                  format(self._node['host']))
262                 else:
263                     logger.error("More instances of VPP running on node {0}.".
264                                  format(self._node['host']))
265             else:
266                 logger.error("VPP not running on node {0}.".
267                              format(self._node['host']))
268             raise RuntimeError("Failed to open VAT console on node {0}".
269                                format(self._node['host']))
270
271         self._exec_failure = False
272         self.vat_stdout = None
273
274     def __enter__(self):
275         return self
276
277     def __exit__(self, exc_type, exc_val, exc_tb):
278         self.vat_terminal_close()
279
280     def vat_terminal_exec_cmd(self, cmd):
281         """Execute command on the opened VAT terminal.
282
283         :param cmd: Command to be executed.
284
285         :returns: Command output in python representation of JSON format or
286             None if not in JSON mode.
287         """
288         PapiHistory.add_to_papi_history(self._node, cmd, papi=False)
289         logger.debug("Executing command in VAT terminal: {0}".format(cmd))
290         try:
291             out = self._ssh.interactive_terminal_exec_command(self._tty, cmd,
292                                                               self.__VAT_PROMPT)
293             self.vat_stdout = out
294         except Exception:
295             self._exec_failure = True
296             vpp_pid = get_vpp_pid(self._node)
297             if vpp_pid:
298                 if isinstance(vpp_pid, int):
299                     raise RuntimeError("VPP running on node {0} but VAT command"
300                                        " {1} execution failed.".
301                                        format(self._node['host'], cmd))
302                 else:
303                     raise RuntimeError("More instances of VPP running on node "
304                                        "{0}. VAT command {1} execution failed.".
305                                        format(self._node['host'], cmd))
306             else:
307                 raise RuntimeError("VPP not running on node {0}. VAT command "
308                                    "{1} execution failed.".
309                                    format(self._node['host'], cmd))
310
311         logger.debug("VAT output: {0}".format(out))
312         if self.json:
313             obj_start = out.find('{')
314             obj_end = out.rfind('}')
315             array_start = out.find('[')
316             array_end = out.rfind(']')
317
318             if obj_start == -1 and array_start == -1:
319                 raise RuntimeError("VAT command {0}: no JSON data.".format(cmd))
320
321             if obj_start < array_start or array_start == -1:
322                 start = obj_start
323                 end = obj_end + 1
324             else:
325                 start = array_start
326                 end = array_end + 1
327             out = out[start:end]
328             json_out = json.loads(out)
329             return json_out
330         else:
331             return None
332
333     def vat_terminal_close(self):
334         """Close VAT terminal."""
335         # interactive terminal is dead, we only need to close session
336         if not self._exec_failure:
337             try:
338                 self._ssh.interactive_terminal_exec_command(self._tty,
339                                                             'quit',
340                                                             self.__LINUX_PROMPT)
341             except Exception:
342                 vpp_pid = get_vpp_pid(self._node)
343                 if vpp_pid:
344                     if isinstance(vpp_pid, int):
345                         logger.trace("VPP running on node {0}.".
346                                      format(self._node['host']))
347                     else:
348                         logger.error("More instances of VPP running on node "
349                                      "{0}.".format(self._node['host']))
350                 else:
351                     logger.error("VPP not running on node {0}.".
352                                  format(self._node['host']))
353                 raise RuntimeError("Failed to close VAT console on node {0}".
354                                    format(self._node['host']))
355         try:
356             self._ssh.interactive_terminal_close(self._tty)
357         except:
358             raise RuntimeError("Cannot close interactive terminal on node {0}".
359                                format(self._node['host']))
360
361     def vat_terminal_exec_cmd_from_template(self, vat_template_file, **args):
362         """Execute VAT script from a file.
363
364         :param vat_template_file: Template file name of a VAT script.
365         :param args: Dictionary of parameters for VAT script.
366         :returns: List of JSON objects returned by VAT.
367         """
368         file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT,
369                                    vat_template_file)
370         with open(file_path, 'r') as template_file:
371             cmd_template = template_file.readlines()
372         ret = []
373         for line_tmpl in cmd_template:
374             vat_cmd = line_tmpl.format(**args)
375             ret.append(self.vat_terminal_exec_cmd(vat_cmd.replace('\n', '')))
376         return ret