Add python.runCmd() to get more details from shell commands

- always print the executing command to control
  the pipeline execution
- always allows to get the stdout/stderr/status
  in the result, even with enabled console enabled
- throws an exception with stderr content, so
  it could be read from the job status and processed

Related task: https://mirantis.jira.com/browse/PROD-31620

Change-Id: I7b0d6ba731c155b36d45fd7dd1b16b756eb7cb27
diff --git a/src/com/mirantis/mk/Python.groovy b/src/com/mirantis/mk/Python.groovy
index 8d9d19b..f144a58 100644
--- a/src/com/mirantis/mk/Python.groovy
+++ b/src/com/mirantis/mk/Python.groovy
@@ -77,6 +77,76 @@
 }
 
 /**
+ * Another command runner to control outputs and exit code
+ *
+ * - always print the executing command to control the pipeline execution
+ * - always allows to get the stdout/stderr/status in the result, even with enabled console enabled
+ * - throws an exception with stderr content, so it could be read from the job status and processed
+ *
+ * @param cmd           String, command to be executed
+ * @param virtualenv    String, path to Python virtualenv (optional, default: '')
+ * @param verbose       Boolean, true: (default) mirror stdout to console and to the result['stdout'] at the same time,
+ *                               false: store stdout only to result['stdout']
+ * @param check_status  Boolean, true: (default) throw an exception which contains result['stderr'] if exit code is not 0,
+ *                               false: only print stderr if not empty, and return the result
+ * @return              Map, ['status' : int, 'stderr' : str, 'stdout' : str ]
+ */
+def runCmd(String cmd, String virtualenv='', Boolean verbose=true, Boolean check_status=true) {
+    def common = new com.mirantis.mk.Common()
+
+    def script
+    def redirect_output
+    def result = [:]
+    def stdout_path = sh(script: '#!/bin/bash +x\nmktemp', returnStdout: true).trim()
+    def stderr_path = sh(script: '#!/bin/bash +x\nmktemp', returnStdout: true).trim()
+
+    if (verbose) {
+        // show stdout to console and store to stdout_path
+        redirect_output = " 1> >(tee -a ${stdout_path}) 2>${stderr_path}"
+    } else {
+        // only store stdout to stdout_path
+        redirect_output = " 1>${stdout_path} 2>${stderr_path}"
+    }
+
+    if (virtualenv) {
+        common.infoMsg("Run shell command in Python virtualenv [${virtualenv}]:\n" + cmd)
+        script = """#!/bin/bash +x
+            . ${virtualenv}/bin/activate
+            ( ${cmd.stripIndent()} ) ${redirect_output}
+        """
+    } else {
+        common.infoMsg('Run shell command:\n' + cmd)
+        script = """#!/bin/bash +x
+            ( ${cmd.stripIndent()} ) ${redirect_output}
+        """
+    }
+
+    result['status'] = sh(script: script, returnStatus: true)
+    result['stdout'] = readFile(stdout_path)
+    result['stderr'] = readFile(stderr_path)
+    def cleanup_script = """#!/bin/bash +x
+        rm ${stdout_path} || true
+        rm ${stderr_path} || true
+    """
+    sh(script: cleanup_script)
+
+    if (result['status'] != 0 && check_status) {
+        def error_message = '\nScript returned exit code: ' + result['status'] + '\n<<<<<< STDERR: >>>>>>\n' + result['stderr']
+        common.errorMsg(error_message)
+        common.printMsg('', 'reset')
+        throw new Exception(error_message)
+    }
+
+    if (result['stderr'] && verbose) {
+        def warning_message = '\nScript returned exit code: ' + result['status'] + '\n<<<<<< STDERR: >>>>>>\n' + result['stderr']
+        common.warningMsg(warning_message)
+        common.printMsg('', 'reset')
+    }
+
+    return result
+}
+
+/**
  * Install docutils in isolated environment
  *
  * @param path Path where virtualenv is created