aboutsummaryrefslogtreecommitdiffstats
path: root/resources/libraries/python/LocalExecution.py
diff options
context:
space:
mode:
Diffstat (limited to 'resources/libraries/python/LocalExecution.py')
-rw-r--r--resources/libraries/python/LocalExecution.py98
1 files changed, 98 insertions, 0 deletions
diff --git a/resources/libraries/python/LocalExecution.py b/resources/libraries/python/LocalExecution.py
new file mode 100644
index 0000000000..bb4cf794a1
--- /dev/null
+++ b/resources/libraries/python/LocalExecution.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2019 Cisco and/or its affiliates.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Python library from executing command on local hosts.
+
+Subprocess offers various functions,
+but there are differences between Python 2 and 3.
+
+Overall, it is more convenient to introduce this internal API
+so call sites are shorter and unified.
+
+This library should support commands given as Iterable, OptionString.
+
+Commands given as a string are explicitly not supported,
+call sites should call .split(" ") on their own risk.
+Similarly, parts within OptionString should not be aggregates.
+Alternatively, long string can be wrapped as 'bash -c "{str}"'.
+Both approaches can be hacked by malicious values.
+"""
+
+import subprocess
+
+from robot.api import logger
+
+from resources.libraries.python.OptionString import OptionString
+
+__all__ = ["run"]
+
+
+MESSAGE_TEMPLATE = "Command {com} ended with RC {ret} and output:\n{out}"
+
+
+def run(command, msg="", check=False, log=True, console=False):
+ """Wrapper around subprocess.check_output that can tolerates nonzero RCs.
+
+ Stderr is redirected to stdout, so it is part of output
+ (but can be mingled as the two streams are buffered independently).
+ If check and rc is nonzero, RuntimeError is raised.
+ If log (and not checked failure), both rc and output are logged.
+ Logging is performed on robot logger. By default .debug(),
+ optionally .console() instead.
+ The default log message is optionally prepended by user-given string,
+ separated by ": ".
+
+ Commands given as single string are not supported, for safety reasons.
+ Invoke bash explicitly if you need its glob support for arguments.
+
+ :param command: List of commands and arguments. Split your long string.
+ :param msg: Message prefix. Argument name is short just to save space.
+ :param check: Whether to raise if return code is nonzero.
+ :param log: Whether to log results.
+ :param console: Whether use .console() instead of .debug().
+ Mainly useful when running from non-main thread.
+ :type command: Iterable or OptionString
+ :type msg: str
+ :type check: bool
+ :type log: bool
+ :type console: bool
+ :returns: rc and output
+ :rtype: 2-tuple of int and str
+ :raises RuntimeError: If check is true and return code non-zero.
+ :raises TypeError: If command is not an iterable.
+ """
+ if isinstance(command, OptionString):
+ command = command.parts
+ if not hasattr(command, "__iter__"):
+ # Strings are indexable, but turning into iterator is not supported.
+ raise TypeError("Command {cmd!r} is not an iterable.".format(
+ cmd=command))
+ ret_code = 0
+ output = ""
+ try:
+ output = subprocess.check_output(command, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as err:
+ output = err.output
+ ret_code = err.returncode
+ if check:
+ raise RuntimeError(MESSAGE_TEMPLATE.format(
+ com=err.cmd, ret=ret_code, out=output))
+ if log:
+ message = MESSAGE_TEMPLATE.format(com=command, ret=ret_code, out=output)
+ if msg:
+ message = msg + ": " + message
+ if console:
+ logger.console(message)
+ else:
+ logger.debug(message)
+ return ret_code, output