7d03fcd4a6961e7f01cb2f5c233791cdd061a372
[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                        copy_on_execute=False):
77         """Execute VAT script on remote node, and store the result. There is an
78         option to copy script from local host to remote host before execution.
79         Path is defined automatically.
80
81         :param vat_name: Name of the vat script file. Only the file name of
82         the script is required, the resources path is prepended automatically.
83         :param node: Node to execute the VAT script on.
84         :param timeout: Seconds to allow the script to run.
85         :param json_out: Require JSON output.
86         :param copy_on_execute: If true, copy the file from local host to remote
87         before executing.
88         :type vat_name: str
89         :type node: dict
90         :type timeout: int
91         :type json_out: bool
92         :type copy_on_execute: bool
93         :raises RuntimeError: If VAT script execution failed.
94         """
95         ssh = SSH()
96         try:
97             ssh.connect(node)
98         except:
99             raise SSHException("Cannot open SSH connection to execute VAT "
100                                "command(s) from vat script {name}"
101                                .format(name=vat_name))
102
103         if copy_on_execute:
104             ssh.scp(vat_name, vat_name)
105             remote_file_path = vat_name
106         else:
107             remote_file_path = '{0}/{1}/{2}'.format(Constants.REMOTE_FW_DIR,
108                                                     Constants.RESOURCES_TPL_VAT,
109                                                     vat_name)
110
111         cmd = "{vat_bin} {json} in {vat_path} script".format(
112             vat_bin=Constants.VAT_BIN_NAME,
113             json="json" if json_out is True else "",
114             vat_path=remote_file_path)
115
116         try:
117             ret_code, stdout, stderr = ssh.exec_command_sudo(cmd=cmd,
118                                                              timeout=timeout)
119         except SSHTimeout:
120             logger.error("VAT script execution timeout: {0}".format(cmd))
121             raise
122         except:
123             raise RuntimeError("VAT script execution failed: {0}".format(cmd))
124
125         self._ret_code = ret_code
126         self._stdout = stdout
127         self._stderr = stderr
128         self._script_name = vat_name
129
130     def execute_script_json_out(self, vat_name, node, timeout=120):
131         """Pass all arguments to 'execute_script' method, then cleanup returned
132         json output.
133
134          :param vat_name: Name of the vat script file. Only the file name of
135         the script is required, the resources path is prepended automatically.
136         :param node: Node to execute the VAT script on.
137         :param timeout: Seconds to allow the script to run.
138         :type vat_name: str
139         :type node: dict
140         :type timeout: int
141         """
142         self.execute_script(vat_name, node, timeout, json_out=True)
143         self._stdout = cleanup_vat_json_output(self._stdout, vat_name=vat_name)
144
145     def script_should_have_failed(self):
146         """Read return code from last executed script and raise exception if the
147         script didn't fail."""
148         if self._ret_code is None:
149             raise Exception("First execute the script!")
150         if self._ret_code == 0:
151             raise AssertionError(
152                 "VAT Script execution passed, but failure was expected: {cmd}"
153                 .format(cmd=self._script_name))
154
155     def script_should_have_passed(self):
156         """Read return code from last executed script and raise exception if the
157         script failed."""
158         if self._ret_code is None:
159             raise Exception("First execute the script!")
160         if self._ret_code != 0:
161             raise AssertionError(
162                 "VAT Script execution failed, but success was expected: {cmd}"
163                 .format(cmd=self._script_name))
164
165     def get_script_stdout(self):
166         """Returns value of stdout from last executed script."""
167         return self._stdout
168
169     def get_script_stderr(self):
170         """Returns value of stderr from last executed script."""
171         return self._stderr
172
173     @staticmethod
174     def cmd_from_template(node, vat_template_file, **vat_args):
175         """Execute VAT script on specified node. This method supports
176         script templates with parameters.
177
178         :param node: Node in topology on witch the script is executed.
179         :param vat_template_file: Template file of VAT script.
180         :param vat_args: Arguments to the template file.
181         :return: List of JSON objects returned by VAT.
182         """
183         with VatTerminal(node) as vat:
184             return vat.vat_terminal_exec_cmd_from_template(vat_template_file,
185                                                            **vat_args)
186
187
188 class VatTerminal(object):
189     """VAT interactive terminal.
190
191     :param node: Node to open VAT terminal on.
192     :param json_param: Defines if outputs from VAT are in JSON format.
193     Default is True.
194     :type node: dict
195     :type json_param: bool
196
197     """
198
199     __VAT_PROMPT = ("vat# ", )
200     __LINUX_PROMPT = (":~$ ", "~]$ ", "~]# ")
201
202     def __init__(self, node, json_param=True):
203         json_text = ' json' if json_param else ''
204         self.json = json_param
205         self._node = node
206         self._ssh = SSH()
207         self._ssh.connect(self._node)
208         try:
209             self._tty = self._ssh.interactive_terminal_open()
210         except Exception:
211             raise RuntimeError("Cannot open interactive terminal on node {0}".
212                                format(self._node))
213
214         for _ in range(3):
215             try:
216                 self._ssh.interactive_terminal_exec_command(
217                     self._tty,
218                     'sudo -S {0}{1}'.format(Constants.VAT_BIN_NAME, json_text),
219                     self.__VAT_PROMPT)
220             except Exception:
221                 continue
222             else:
223                 break
224         else:
225             vpp_pid = get_vpp_pid(self._node)
226             if vpp_pid:
227                 if isinstance(vpp_pid, int):
228                     logger.trace("VPP running on node {0}".
229                                  format(self._node['host']))
230                 else:
231                     logger.error("More instances of VPP running on node {0}.".
232                                  format(self._node['host']))
233             else:
234                 logger.error("VPP not running on node {0}.".
235                              format(self._node['host']))
236             raise RuntimeError("Failed to open VAT console on node {0}".
237                                format(self._node['host']))
238
239         self._exec_failure = False
240         self.vat_stdout = None
241
242     def __enter__(self):
243         return self
244
245     def __exit__(self, exc_type, exc_val, exc_tb):
246         self.vat_terminal_close()
247
248     def vat_terminal_exec_cmd(self, cmd):
249         """Execute command on the opened VAT terminal.
250
251         :param cmd: Command to be executed.
252
253         :return: Command output in python representation of JSON format or
254         None if not in JSON mode.
255         """
256         VatHistory.add_to_vat_history(self._node, cmd)
257         logger.debug("Executing command in VAT terminal: {0}".format(cmd))
258         try:
259             out = self._ssh.interactive_terminal_exec_command(self._tty, cmd,
260                                                               self.__VAT_PROMPT)
261             self.vat_stdout = out
262         except Exception:
263             self._exec_failure = True
264             vpp_pid = get_vpp_pid(self._node)
265             if vpp_pid:
266                 if isinstance(vpp_pid, int):
267                     raise RuntimeError("VPP running on node {0} but VAT command"
268                                        " {1} execution failed.".
269                                        format(self._node['host'], cmd))
270                 else:
271                     raise RuntimeError("More instances of VPP running on node "
272                                        "{0}. VAT command {1} execution failed.".
273                                        format(self._node['host'], cmd))
274             else:
275                 raise RuntimeError("VPP not running on node {0}. VAT command "
276                                    "{1} execution failed.".
277                                    format(self._node['host'], cmd))
278
279         logger.debug("VAT output: {0}".format(out))
280         if self.json:
281             obj_start = out.find('{')
282             obj_end = out.rfind('}')
283             array_start = out.find('[')
284             array_end = out.rfind(']')
285
286             if obj_start == -1 and array_start == -1:
287                 raise RuntimeError("VAT command {0}: no JSON data.".format(cmd))
288
289             if obj_start < array_start or array_start == -1:
290                 start = obj_start
291                 end = obj_end + 1
292             else:
293                 start = array_start
294                 end = array_end + 1
295             out = out[start:end]
296             json_out = json.loads(out)
297             return json_out
298         else:
299             return None
300
301     def vat_terminal_close(self):
302         """Close VAT terminal."""
303         # interactive terminal is dead, we only need to close session
304         if not self._exec_failure:
305             try:
306                 self._ssh.interactive_terminal_exec_command(self._tty,
307                                                             'quit',
308                                                             self.__LINUX_PROMPT)
309             except Exception:
310                 vpp_pid = get_vpp_pid(self._node)
311                 if vpp_pid:
312                     if isinstance(vpp_pid, int):
313                         logger.trace("VPP running on node {0}.".
314                                      format(self._node['host']))
315                     else:
316                         logger.error("More instances of VPP running on node "
317                                      "{0}.".format(self._node['host']))
318                 else:
319                     logger.error("VPP not running on node {0}.".
320                                  format(self._node['host']))
321                 raise RuntimeError("Failed to close VAT console on node {0}".
322                                    format(self._node['host']))
323         try:
324             self._ssh.interactive_terminal_close(self._tty)
325         except:
326             raise RuntimeError("Cannot close interactive terminal on node {0}".
327                                format(self._node['host']))
328
329     def vat_terminal_exec_cmd_from_template(self, vat_template_file, **args):
330         """Execute VAT script from a file.
331
332         :param vat_template_file: Template file name of a VAT script.
333         :param args: Dictionary of parameters for VAT script.
334         :return: List of JSON objects returned by VAT.
335         """
336         file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT,
337                                    vat_template_file)
338         with open(file_path, 'r') as template_file:
339             cmd_template = template_file.readlines()
340         ret = []
341         for line_tmpl in cmd_template:
342             vat_cmd = line_tmpl.format(**args)
343             ret.append(self.vat_terminal_exec_cmd(vat_cmd.replace('\n', '')))
344         return ret