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