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