feat(jobspec): Unify soak jobspecs
[csit.git] / resources / libraries / python / VatExecutor.py
1 # Copyright (c) 2020 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 import resources.libraries.python.DUTSetup as PidLib
24
25 from resources.libraries.python.Constants import Constants
26 from resources.libraries.python.PapiHistory import PapiHistory
27 from resources.libraries.python.ssh import SSH, SSHTimeout
28
29 __all__ = [u"VatExecutor"]
30
31
32 def cleanup_vat_json_output(json_output, vat_name=None):
33     """Return VAT JSON output cleaned from VAT clutter.
34
35     Clean up VAT JSON output from clutter like vat# prompts and such.
36
37     :param json_output: Cluttered JSON output.
38     :param vat_name: Name of the VAT script.
39     :type json_output: JSON
40     :type vat_name: str
41     :returns: Cleaned up output JSON string.
42     :rtype: JSON
43     """
44
45     retval = json_output
46     clutter = [u"vat#", u"dump_interface_table error: Misc"]
47     if vat_name:
48         remote_file_path = f"{Constants.REMOTE_FW_DIR}/" \
49             f"{Constants.RESOURCES_TPL_VAT}/{vat_name}"
50         clutter.append(f"{remote_file_path}(2):")
51     for garbage in clutter:
52         retval = retval.replace(garbage, u"")
53     return retval
54
55
56 def get_vpp_pid(node):
57     """Get PID of running VPP process.
58
59     :param node: DUT node.
60     :type node: dict
61     :returns: PID of VPP process / List of PIDs if more VPP processes are
62         running on the DUT node.
63     :rtype: int or list
64     """
65     pid = PidLib.DUTSetup.get_pid(node, u"vpp")
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, u"rt") 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, u"wt") 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 "
255                 f"{self._node[u'host']}"
256             )
257
258         for _ in range(3):
259             try:
260                 self._ssh.interactive_terminal_exec_command(
261                     self._tty, f"sudo -S {Constants.VAT_BIN_NAME}{json_text}",
262                     self.__VAT_PROMPT
263                 )
264             except Exception:
265                 continue
266             else:
267                 break
268         else:
269             vpp_pid = get_vpp_pid(self._node)
270             if vpp_pid:
271                 if isinstance(vpp_pid, int):
272                     logger.trace(f"VPP running on node {self._node[u'host']}")
273                 else:
274                     logger.error(
275                         f"More instances of VPP running "
276                         f"on node {self._node[u'host']}."
277                     )
278             else:
279                 logger.error(f"VPP not running on node {self._node[u'host']}.")
280             raise RuntimeError(
281                 f"Failed to open VAT console on node {self._node[u'host']}"
282             )
283
284         self._exec_failure = False
285         self.vat_stdout = None
286
287     def __enter__(self):
288         return self
289
290     def __exit__(self, exc_type, exc_val, exc_tb):
291         self.vat_terminal_close()
292
293     def vat_terminal_exec_cmd(self, cmd):
294         """Execute command on the opened VAT terminal.
295
296         :param cmd: Command to be executed.
297
298         :returns: Command output in python representation of JSON format or
299             None if not in JSON mode.
300         """
301         PapiHistory.add_to_papi_history(self._node, cmd, papi=False)
302         logger.debug(f"Executing command in VAT terminal: {cmd}")
303         try:
304             out = self._ssh.interactive_terminal_exec_command(
305                 self._tty, cmd, self.__VAT_PROMPT
306             )
307             self.vat_stdout = out
308         except Exception:
309             self._exec_failure = True
310             vpp_pid = get_vpp_pid(self._node)
311             if vpp_pid:
312                 if isinstance(vpp_pid, int):
313                     msg = f"VPP running on node {self._node[u'host']} " \
314                         f"but VAT command {cmd} execution failed."
315                 else:
316                     msg = f"More instances of VPP running on node " \
317                         f"{self._node[u'host']}. VAT command {cmd} " \
318                         f"execution failed."
319             else:
320                 msg = f"VPP not running on node {self._node[u'host']}. " \
321                     f"VAT command {cmd} execution failed."
322             raise RuntimeError(msg)
323
324         logger.debug(f"VAT output: {out}")
325         if self.json:
326             obj_start = out.find(u"{")
327             obj_end = out.rfind(u"}")
328             array_start = out.find(u"[")
329             array_end = out.rfind(u"]")
330
331             if obj_start == -1 and array_start == -1:
332                 raise RuntimeError(f"VAT command {cmd}: no JSON data.")
333
334             if obj_start < array_start or array_start == -1:
335                 start = obj_start
336                 end = obj_end + 1
337             else:
338                 start = array_start
339                 end = array_end + 1
340             out = out[start:end]
341             json_out = json.loads(out)
342             return json_out
343
344         return None
345
346     def vat_terminal_close(self):
347         """Close VAT terminal."""
348         # interactive terminal is dead, we only need to close session
349         if not self._exec_failure:
350             try:
351                 self._ssh.interactive_terminal_exec_command(
352                     self._tty, u"quit", self.__LINUX_PROMPT
353                 )
354             except Exception:
355                 vpp_pid = get_vpp_pid(self._node)
356                 if vpp_pid:
357                     if isinstance(vpp_pid, int):
358                         logger.trace(
359                             f"VPP running on node {self._node[u'host']}."
360                         )
361                     else:
362                         logger.error(
363                             f"More instances of VPP running "
364                             f"on node {self._node[u'host']}."
365                         )
366                 else:
367                     logger.error(
368                         f"VPP not running on node {self._node[u'host']}."
369                     )
370                 raise RuntimeError(
371                     f"Failed to close VAT console "
372                     f"on node {self._node[u'host']}"
373                 )
374         try:
375             self._ssh.interactive_terminal_close(self._tty)
376         except Exception:
377             raise RuntimeError(
378                 f"Cannot close interactive terminal "
379                 f"on node {self._node[u'host']}"
380             )
381
382     def vat_terminal_exec_cmd_from_template(self, vat_template_file, **args):
383         """Execute VAT script from a file.
384
385         :param vat_template_file: Template file name of a VAT script.
386         :param args: Dictionary of parameters for VAT script.
387         :returns: List of JSON objects returned by VAT.
388         """
389         file_path = f"{Constants.RESOURCES_TPL_VAT}/{vat_template_file}"
390
391         with open(file_path, u"rt") as template_file:
392             cmd_template = template_file.readlines()
393         ret = list()
394         for line_tmpl in cmd_template:
395             vat_cmd = line_tmpl.format(**args)
396             ret.append(self.vat_terminal_exec_cmd(vat_cmd.replace(u"\n", u"")))
397         return ret