New shell_cmd

- shell_cmd is intended as a replacement for cmd_fnc.  It has the
  following additional features:
  - Support for the following additional arguments:
    - timeout
    - max_attempts
    - retry_sleep_time
    - allowed_shell_rcs
    - ignore_err which defaults to the ignore_err variable found when
      searching upward in the call stack.
  - The t_shell_cmd alias which effectively sets the test_mode parm to
    the test_mode variable value found when searching upward in the call
    stack.

Change-Id: Ib96e6d9e459728277c16b139ca87f5842bcbc14f
Signed-off-by: Michael Walsh <micwalsh@us.ibm.com>
diff --git a/lib/gen_cmd.py b/lib/gen_cmd.py
index f9bd3cf..ff30ecd 100644
--- a/lib/gen_cmd.py
+++ b/lib/gen_cmd.py
@@ -4,9 +4,12 @@
 This module provides command execution functions such as cmd_fnc and cmd_fnc_u.
 """
 
+import os
 import sys
 import subprocess
 import collections
+import signal
+import time
 
 import gen_print as gp
 import gen_valid as gv
@@ -19,6 +22,8 @@
     from robot.libraries.BuiltIn import BuiltIn
 
 
+# cmd_fnc and cmd_fnc_u should now be considered deprecated.  shell_cmd and
+# t_shell_cmd should be used instead.
 def cmd_fnc(cmd_buf,
             quiet=None,
             test_mode=None,
@@ -249,3 +254,236 @@
         ix += 1
 
     return command_string_dict
+
+
+# Save the original SIGALRM handler for later restoration by shell_cmd.
+original_sigalrm_handler = signal.getsignal(signal.SIGALRM)
+
+
+def shell_cmd_timed_out(signal_number,
+                        frame):
+    r"""
+    Handle an alarm signal generated during the shell_cmd function.
+    """
+
+    gp.dprint_executing()
+    # Get subprocess pid from shell_cmd's call stack.
+    sub_proc = gp.get_stack_var('sub_proc', 0)
+    pid = sub_proc.pid
+    # Terminate the child process.
+    os.kill(pid, signal.SIGTERM)
+    # Restore the original SIGALRM handler.
+    signal.signal(signal.SIGALRM, original_sigalrm_handler)
+
+    return
+
+
+def shell_cmd(command_string,
+              quiet=None,
+              print_output=1,
+              show_err=1,
+              test_mode=0,
+              time_out=None,
+              max_attempts=1,
+              retry_sleep_time=5,
+              allowed_shell_rcs=[0],
+              ignore_err=None,
+              return_stderr=0):
+    r"""
+    Run the given command string in a shell and return a tuple consisting of
+    the shell return code and the output.
+
+    Description of argument(s):
+    command_string                  The command string to be run in a shell
+                                    (e.g. "ls /tmp").
+    quiet                           If set to 0, this function will print
+                                    "Issuing: <cmd string>" to stdout.
+    print_output                    If this is set, this function will print
+                                    the stdout/stderr generated by the shell
+                                    command to stdout.
+    show_err                        If show_err is set, this function will
+                                    print a standardized error report if the
+                                    shell command fails (i.e. if the shell
+                                    command returns a shell_rc that is not in
+                                    allowed_shell_rcs).  Note: Error text is
+                                    only printed if ALL attempts to run the
+                                    command_string fail.  In other words, if
+                                    the command execution is ultimately
+                                    successful, initial failures are hidden.
+    test_mode                       If test_mode is set, this function will
+                                    not actually run the command.  If
+                                    print_output is also set, this function
+                                    will print "(test_mode) Issuing: <cmd
+                                    string>" to stdout.  A caller should call
+                                    shell_cmd directly if they wish to have
+                                    the command string run unconditionally.
+                                    They should call the t_shell_cmd wrapper
+                                    (defined below) if they wish to run the
+                                    command string only if the prevailing
+                                    test_mode variable is set to 0.
+    time_out                        A time-out value expressed in seconds.  If
+                                    the command string has not finished
+                                    executing within <time_out> seconds, it
+                                    will be halted and counted as an error.
+    max_attempts                    The max number of attempts that should be
+                                    made to run the command string.
+    retry_sleep_time                The number of seconds to sleep between
+                                    attempts.
+    allowed_shell_rcs               A list of integers indicating which
+                                    shell_rc values are not to be considered
+                                    errors.
+    ignore_err                      Ignore error means that a failure
+                                    encountered by running the command string
+                                    will not be raised as a python exception.
+                                    When the ignore_err argument is set to
+                                    None, this function will assign a default
+                                    value by searching upward in the stack for
+                                    the ignore_err variable value.  If no such
+                                    value is found, ignore_err is set to 1.
+    return_stderr                   If return_stderr is set, this function
+                                    will process the stdout and stderr streams
+                                    from the shell command separately.  In
+                                    such a case, the tuple returned by this
+                                    function will consist of three values
+                                    rather than just two: rc, stdout, stderr.
+    """
+
+    # Assign default values to some of the arguments to this function.
+    quiet = int(gp.get_var_value(quiet, gp.get_stack_var('quiet', 0)))
+    test_mode = int(gp.get_var_value(test_mode, gp.get_stack_var('test_mode',
+                                                                 0)))
+    ignore_err = int(gp.get_var_value(ignore_err,
+                                      gp.get_stack_var('ignore_err', 1)))
+
+    err_msg = gv.svalid_value(command_string)
+    if err_msg != "":
+        raise ValueError(err_msg)
+
+    if not quiet:
+        gp.print_issuing(command_string, test_mode)
+
+    if test_mode:
+        if return_stderr:
+            return 0, "", ""
+        else:
+            return 0, ""
+
+    # Convert each list entry to a signed value.
+    allowed_shell_rcs = [gm.to_signed(x) for x in allowed_shell_rcs]
+
+    if return_stderr:
+        stderr = subprocess.PIPE
+    else:
+        stderr = subprocess.STDOUT
+
+    shell_rc = 0
+    out_buf = ""
+    err_buf = ""
+    # Write all output to func_history_stdout rather than directly to stdout.
+    # This allows us to decide what to print after all attempts to run the
+    # command string have been made.  func_history_stdout will contain the
+    # complete stdout history from the current invocation of this function.
+    func_history_stdout = ""
+    for attempt_num in range(1, max_attempts + 1):
+        sub_proc = subprocess.Popen(command_string,
+                                    bufsize=1,
+                                    shell=True,
+                                    executable='/bin/bash',
+                                    stdout=subprocess.PIPE,
+                                    stderr=stderr)
+        out_buf = ""
+        err_buf = ""
+        # Output from this loop iteration is written to func_stdout for later
+        # processing.
+        func_stdout = ""
+        command_timed_out = False
+        if time_out is not None:
+            # Designate a SIGALRM handling function and set alarm.
+            signal.signal(signal.SIGALRM, shell_cmd_timed_out)
+            signal.alarm(time_out)
+        try:
+            if return_stderr:
+                for line in sub_proc.stderr:
+                    err_buf += line
+                    if not print_output:
+                        continue
+                    func_stdout += line
+            for line in sub_proc.stdout:
+                out_buf += line
+                if not print_output:
+                    continue
+                func_stdout += line
+        except IOError:
+            command_timed_out = True
+        sub_proc.communicate()
+        shell_rc = sub_proc.returncode
+        # Restore the original SIGALRM handler and clear the alarm.
+        signal.signal(signal.SIGALRM, original_sigalrm_handler)
+        signal.alarm(0)
+        if shell_rc in allowed_shell_rcs:
+            break
+        err_msg = "The prior shell command failed.\n"
+        if command_timed_out:
+            err_msg += gp.sprint_var(command_timed_out)
+            err_msg += gp.sprint_var(time_out)
+            err_msg += gp.sprint_varx("child_pid", sub_proc.pid)
+        err_msg += gp.sprint_var(attempt_num)
+        err_msg += gp.sprint_var(shell_rc, 1)
+        err_msg += gp.sprint_var(allowed_shell_rcs, 1)
+        if not print_output:
+            if return_stderr:
+                err_msg += "err_buf:\n" + err_buf
+            err_msg += "out_buf:\n" + out_buf
+        if show_err:
+            if robot_env:
+                func_stdout += grp.sprint_error_report(err_msg)
+            else:
+                func_stdout += gp.sprint_error_report(err_msg)
+        func_history_stdout += func_stdout
+        if attempt_num < max_attempts:
+            func_history_stdout += gp.sprint_issuing("time.sleep(" +
+                                                     str(retry_sleep_time) +
+                                                     ")")
+            time.sleep(retry_sleep_time)
+
+    if shell_rc not in allowed_shell_rcs:
+        func_stdout = func_history_stdout
+
+    if robot_env:
+        grp.rprint(func_stdout)
+    else:
+        sys.stdout.write(func_stdout)
+        sys.stdout.flush()
+
+    if shell_rc not in allowed_shell_rcs:
+        if not ignore_err:
+            if robot_env:
+                BuiltIn().fail(err_msg)
+            else:
+                raise ValueError("The prior shell command failed.\n")
+
+    if return_stderr:
+        return shell_rc, out_buf, err_buf
+    else:
+        return shell_rc, out_buf
+
+
+def t_shell_cmd(command_string, **kwargs):
+    r"""
+    Search upward in the the call stack to obtain the test_mode argument, add
+    it to kwargs and then call shell_cmd and return the result.
+
+    See shell_cmd prolog for details on all arguments.
+    """
+
+    if 'test_mode' in kwargs:
+        error_message = "Programmer error - test_mode is not a valid" +\
+            " argument to this function."
+        gp.print_error_report(error_message)
+        exit(1)
+
+    test_mode = gp.get_stack_var('test_mode',
+                                 int(gp.get_var_value(None, 0, "test_mode")))
+    kwargs['test_mode'] = test_mode
+
+    return shell_cmd(command_string, **kwargs)