perf: add TCP Nginx+LDPRELOAD suites
[csit.git] / resources / libraries / python / LocalExecution.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 """Python library from executing command on local hosts.
15
16 Subprocess offers various functions,
17 but there are differences between Python 2 and 3.
18
19 Overall, it is more convenient to introduce this internal API
20 so call sites are shorter and unified.
21
22 This library should support commands given as Iterable, OptionString.
23
24 Commands given as a string are explicitly not supported,
25 call sites should call .split(" ") on their own risk.
26 Similarly, parts within OptionString should not be aggregates.
27 Alternatively, long string can be wrapped as 'bash -c "{str}"'.
28 Both approaches can be hacked by malicious values.
29 """
30
31 import subprocess
32
33 from robot.api import logger
34
35 from resources.libraries.python.OptionString import OptionString
36
37 __all__ = [u"run"]
38
39
40 MESSAGE_TEMPLATE = u"Command {com} ended with RC {ret} and output:\n{out}"
41
42
43 def run(command, msg=u"", check=True, log=False, console=False):
44     """Wrapper around subprocess.check_output that can tolerates nonzero RCs.
45
46     Stderr is redirected to stdout, so it is part of output
47     (but can be mingled as the two streams are buffered independently).
48     If check and rc is nonzero, RuntimeError is raised.
49     If log (and not checked failure), both rc and output are logged.
50     Logging is performed on robot logger. By default .debug(),
51     optionally .console() instead.
52     The default log message is optionally prepended by user-given string,
53     separated by ": ".
54
55     Commands given as single string are not supported, for safety reasons.
56     Invoke bash explicitly if you need its glob support for arguments.
57
58     :param command: List of commands and arguments. Split your long string.
59     :param msg: Message prefix. Argument name is short just to save space.
60     :param check: Whether to raise if return code is nonzero.
61     :param log: Whether to log results.
62     :param console: Whether use .console() instead of .debug().
63         Mainly useful when running from non-main thread.
64     :type command: Iterable or OptionString
65     :type msg: str
66     :type check: bool
67     :type log: bool
68     :type console: bool
69     :returns: rc and output
70     :rtype: 2-tuple of int and str
71     :raises RuntimeError: If check is true and return code non-zero.
72     :raises TypeError: If command is not an iterable.
73     """
74     if isinstance(command, OptionString):
75         command = command.parts
76     if not hasattr(command, u"__iter__"):
77         # Strings are indexable, but turning into iterator is not supported.
78         raise TypeError(f"Command {command!r} is not an iterable.")
79     ret_code = 0
80     output = u""
81     try:
82         output = subprocess.check_output(command, stderr=subprocess.STDOUT)
83     except subprocess.CalledProcessError as err:
84         output = err.output
85         ret_code = err.returncode
86         if check:
87             raise RuntimeError(
88                 MESSAGE_TEMPLATE.format(com=err.cmd, ret=ret_code, out=output)
89             )
90     if log:
91         message = MESSAGE_TEMPLATE.format(com=command, ret=ret_code, out=output)
92         if msg:
93             message = f"{msg}: {message}"
94         if console:
95             logger.console(message)
96         else:
97             logger.debug(message)
98     return ret_code, output