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