1#!/usr/bin/env python
2
3r"""
4This module provides command execution functions such as cmd_fnc and cmd_fnc_u.
5"""
6
7import os
8import sys
9import subprocess
10import collections
11import signal
12import time
13
14import gen_print as gp
15import gen_valid as gv
16import gen_misc as gm
17
18robot_env = gp.robot_env
19
20if robot_env:
21    import gen_robot_print as grp
22    from robot.libraries.BuiltIn import BuiltIn
23
24
25# cmd_fnc and cmd_fnc_u should now be considered deprecated.  shell_cmd and
26# t_shell_cmd should be used instead.
27def cmd_fnc(cmd_buf,
28            quiet=None,
29            test_mode=None,
30            debug=0,
31            print_output=1,
32            show_err=1,
33            return_stderr=0,
34            ignore_err=1):
35    r"""
36    Run the given command in a shell and return the shell return code and the
37    output.
38
39    Description of arguments:
40    cmd_buf                         The command string to be run in a shell.
41    quiet                           Indicates whether this function should run
42                                    the print_issuing() function which prints
43                                    "Issuing: <cmd string>" to stdout.
44    test_mode                       If test_mode is set, this function will
45                                    not actually run the command.  If
46                                    print_output is set, it will print
47                                    "(test_mode) Issuing: <cmd string>" to
48                                    stdout.
49    debug                           If debug is set, this function will print
50                                    extra debug info.
51    print_output                    If this is set, this function will print
52                                    the stdout/stderr generated by the shell
53                                    command.
54    show_err                        If show_err is set, this function will
55                                    print a standardized error report if the
56                                    shell command returns non-zero.
57    return_stderr                   If return_stderr is set, this function
58                                    will process the stdout and stderr streams
59                                    from the shell command separately.  It
60                                    will also return stderr in addition to the
61                                    return code and the stdout.
62    """
63
64    # Determine default values.
65    quiet = int(gm.global_default(quiet, 0))
66    test_mode = int(gm.global_default(test_mode, 0))
67
68    if debug:
69        gp.print_vars(cmd_buf, quiet, test_mode, debug)
70
71    err_msg = gv.svalid_value(cmd_buf)
72    if err_msg != "":
73        raise ValueError(err_msg)
74
75    if not quiet:
76        gp.pissuing(cmd_buf, test_mode)
77
78    if test_mode:
79        if return_stderr:
80            return 0, "", ""
81        else:
82            return 0, ""
83
84    if return_stderr:
85        err_buf = ""
86        stderr = subprocess.PIPE
87    else:
88        stderr = subprocess.STDOUT
89
90    sub_proc = subprocess.Popen(cmd_buf,
91                                bufsize=1,
92                                shell=True,
93                                executable='/bin/bash',
94                                stdout=subprocess.PIPE,
95                                stderr=stderr)
96    out_buf = ""
97    if return_stderr:
98        for line in sub_proc.stderr:
99            err_buf += line
100            if not print_output:
101                continue
102            if robot_env:
103                grp.rprint(line)
104            else:
105                sys.stdout.write(line)
106    for line in sub_proc.stdout:
107        out_buf += line
108        if not print_output:
109            continue
110        if robot_env:
111            grp.rprint(line)
112        else:
113            sys.stdout.write(line)
114    if print_output and not robot_env:
115        sys.stdout.flush()
116    sub_proc.communicate()
117    shell_rc = sub_proc.returncode
118    if shell_rc != 0:
119        err_msg = "The prior shell command failed.\n"
120        err_msg += gp.sprint_var(shell_rc, 1)
121        if not print_output:
122            err_msg += "out_buf:\n" + out_buf
123
124        if show_err:
125            if robot_env:
126                grp.rprint_error_report(err_msg)
127            else:
128                gp.print_error_report(err_msg)
129        if not ignore_err:
130            if robot_env:
131                BuiltIn().fail(err_msg)
132            else:
133                raise ValueError(err_msg)
134
135    if return_stderr:
136        return shell_rc, out_buf, err_buf
137    else:
138        return shell_rc, out_buf
139
140
141def cmd_fnc_u(cmd_buf,
142              quiet=None,
143              debug=None,
144              print_output=1,
145              show_err=1,
146              return_stderr=0,
147              ignore_err=1):
148    r"""
149    Call cmd_fnc with test_mode=0.  See cmd_fnc (above) for details.
150
151    Note the "u" in "cmd_fnc_u" stands for "unconditional".
152    """
153
154    return cmd_fnc(cmd_buf, test_mode=0, quiet=quiet, debug=debug,
155                   print_output=print_output, show_err=show_err,
156                   return_stderr=return_stderr, ignore_err=ignore_err)
157
158
159def parse_command_string(command_string):
160    r"""
161    Parse a bash command-line command string and return the result as a
162    dictionary of parms.
163
164    This can be useful for answering questions like "What did the user specify
165    as the value for parm x in the command string?".
166
167    This function expects the command string to follow the following posix
168    conventions:
169    - Short parameters:
170      -<parm name><space><arg value>
171    - Long parameters:
172      --<parm name>=<arg value>
173
174    The first item in the string will be considered to be the command.  All
175    values not conforming to the specifications above will be considered
176    positional parms.  If there are multiple parms with the same name, they
177    will be put into a list (see illustration below where "-v" is specified
178    multiple times).
179
180    Description of argument(s):
181    command_string                  The complete command string including all
182                                    parameters and arguments.
183
184    Sample input:
185
186    robot_cmd_buf:                                    robot -v
187    OPENBMC_HOST:dummy1 -v keyword_string:'Set Auto Reboot  no' -v
188    lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot -v
189    quiet:0 -v test_mode:0 -v debug:0
190    --outputdir='/home/user1/status/children/'
191    --output=dummy1.Auto_reboot.170802.124544.output.xml
192    --log=dummy1.Auto_reboot.170802.124544.log.html
193    --report=dummy1.Auto_reboot.170802.124544.report.html
194    /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
195
196    Sample output:
197
198    robot_cmd_buf_dict:
199      robot_cmd_buf_dict[command]:                    robot
200      robot_cmd_buf_dict[v]:
201        robot_cmd_buf_dict[v][0]:                     OPENBMC_HOST:dummy1
202        robot_cmd_buf_dict[v][1]:                     keyword_string:Set Auto
203        Reboot no
204        robot_cmd_buf_dict[v][2]:
205        lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot
206        robot_cmd_buf_dict[v][3]:                     quiet:0
207        robot_cmd_buf_dict[v][4]:                     test_mode:0
208        robot_cmd_buf_dict[v][5]:                     debug:0
209      robot_cmd_buf_dict[outputdir]:
210      /home/user1/status/children/
211      robot_cmd_buf_dict[output]:
212      dummy1.Auto_reboot.170802.124544.output.xml
213      robot_cmd_buf_dict[log]:
214      dummy1.Auto_reboot.170802.124544.log.html
215      robot_cmd_buf_dict[report]:
216      dummy1.Auto_reboot.170802.124544.report.html
217      robot_cmd_buf_dict[positional]:
218      /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
219    """
220
221    # We want the parms in the string broken down the way bash would do it,
222    # so we'll call upon bash to do that by creating a simple inline bash
223    # function.
224    bash_func_def = "function parse { for parm in \"${@}\" ; do" +\
225        " echo $parm ; done ; }"
226
227    rc, outbuf = cmd_fnc_u(bash_func_def + " ; parse " + command_string,
228                           quiet=1, print_output=0)
229    command_string_list = outbuf.rstrip("\n").split("\n")
230
231    command_string_dict = collections.OrderedDict()
232    ix = 1
233    command_string_dict['command'] = command_string_list[0]
234    while ix < len(command_string_list):
235        if command_string_list[ix].startswith("--"):
236            key, value = command_string_list[ix].split("=")
237            key = key.lstrip("-")
238        elif command_string_list[ix].startswith("-"):
239            key = command_string_list[ix].lstrip("-")
240            ix += 1
241            try:
242                value = command_string_list[ix]
243            except IndexError:
244                value = ""
245        else:
246            key = 'positional'
247            value = command_string_list[ix]
248        if key in command_string_dict:
249            if isinstance(command_string_dict[key], str):
250                command_string_dict[key] = [command_string_dict[key]]
251            command_string_dict[key].append(value)
252        else:
253            command_string_dict[key] = value
254        ix += 1
255
256    return command_string_dict
257
258
259# Save the original SIGALRM handler for later restoration by shell_cmd.
260original_sigalrm_handler = signal.getsignal(signal.SIGALRM)
261
262
263def shell_cmd_timed_out(signal_number,
264                        frame):
265    r"""
266    Handle an alarm signal generated during the shell_cmd function.
267    """
268
269    gp.dprint_executing()
270    # Get subprocess pid from shell_cmd's call stack.
271    sub_proc = gp.get_stack_var('sub_proc', 0)
272    pid = sub_proc.pid
273    # Terminate the child process.
274    os.kill(pid, signal.SIGTERM)
275    # Restore the original SIGALRM handler.
276    signal.signal(signal.SIGALRM, original_sigalrm_handler)
277
278    return
279
280
281def shell_cmd(command_string,
282              quiet=None,
283              print_output=None,
284              show_err=1,
285              test_mode=0,
286              time_out=None,
287              max_attempts=1,
288              retry_sleep_time=5,
289              allowed_shell_rcs=[0],
290              ignore_err=None,
291              return_stderr=0,
292              fork=0):
293    r"""
294    Run the given command string in a shell and return a tuple consisting of
295    the shell return code and the output.
296
297    Description of argument(s):
298    command_string                  The command string to be run in a shell
299                                    (e.g. "ls /tmp").
300    quiet                           If set to 0, this function will print
301                                    "Issuing: <cmd string>" to stdout.  When
302                                    the quiet argument is set to None, this
303                                    function will assign a default value by
304                                    searching upward in the stack for the
305                                    quiet variable value.  If no such value is
306                                    found, quiet is set to 0.
307    print_output                    If this is set, this function will print
308                                    the stdout/stderr generated by the shell
309                                    command to stdout.
310    show_err                        If show_err is set, this function will
311                                    print a standardized error report if the
312                                    shell command fails (i.e. if the shell
313                                    command returns a shell_rc that is not in
314                                    allowed_shell_rcs).  Note: Error text is
315                                    only printed if ALL attempts to run the
316                                    command_string fail.  In other words, if
317                                    the command execution is ultimately
318                                    successful, initial failures are hidden.
319    test_mode                       If test_mode is set, this function will
320                                    not actually run the command.  If
321                                    print_output is also set, this function
322                                    will print "(test_mode) Issuing: <cmd
323                                    string>" to stdout.  A caller should call
324                                    shell_cmd directly if they wish to have
325                                    the command string run unconditionally.
326                                    They should call the t_shell_cmd wrapper
327                                    (defined below) if they wish to run the
328                                    command string only if the prevailing
329                                    test_mode variable is set to 0.
330    time_out                        A time-out value expressed in seconds.  If
331                                    the command string has not finished
332                                    executing within <time_out> seconds, it
333                                    will be halted and counted as an error.
334    max_attempts                    The max number of attempts that should be
335                                    made to run the command string.
336    retry_sleep_time                The number of seconds to sleep between
337                                    attempts.
338    allowed_shell_rcs               A list of integers indicating which
339                                    shell_rc values are not to be considered
340                                    errors.
341    ignore_err                      Ignore error means that a failure
342                                    encountered by running the command string
343                                    will not be raised as a python exception.
344                                    When the ignore_err argument is set to
345                                    None, this function will assign a default
346                                    value by searching upward in the stack for
347                                    the ignore_err variable value.  If no such
348                                    value is found, ignore_err is set to 1.
349    return_stderr                   If return_stderr is set, this function
350                                    will process the stdout and stderr streams
351                                    from the shell command separately.  In
352                                    such a case, the tuple returned by this
353                                    function will consist of three values
354                                    rather than just two: rc, stdout, stderr.
355    fork                            Run the command string asynchronously
356                                    (i.e. don't wait for status of the child
357                                    process and don't try to get
358                                    stdout/stderr).
359    """
360
361    # Assign default values to some of the arguments to this function.
362    quiet = int(gm.dft(quiet, gp.get_stack_var('quiet', 0)))
363    print_output = int(gm.dft(print_output, not quiet))
364    show_err = int(show_err)
365    ignore_err = int(gm.dft(ignore_err,
366                            gp.get_stack_var('ignore_err',
367                                             gp.get_var_value(ignore_err, 1))))
368
369    err_msg = gv.svalid_value(command_string)
370    if err_msg != "":
371        raise ValueError(err_msg)
372
373    if not quiet:
374        gp.print_issuing(command_string, test_mode)
375
376    if test_mode:
377        if return_stderr:
378            return 0, "", ""
379        else:
380            return 0, ""
381
382    # Convert each list entry to a signed value.
383    allowed_shell_rcs = [gm.to_signed(x) for x in allowed_shell_rcs]
384
385    if return_stderr:
386        stderr = subprocess.PIPE
387    else:
388        stderr = subprocess.STDOUT
389
390    shell_rc = 0
391    out_buf = ""
392    err_buf = ""
393    # Write all output to func_history_stdout rather than directly to stdout.
394    # This allows us to decide what to print after all attempts to run the
395    # command string have been made.  func_history_stdout will contain the
396    # complete stdout history from the current invocation of this function.
397    func_history_stdout = ""
398    for attempt_num in range(1, max_attempts + 1):
399        sub_proc = subprocess.Popen(command_string,
400                                    bufsize=1,
401                                    shell=True,
402                                    executable='/bin/bash',
403                                    stdout=subprocess.PIPE,
404                                    stderr=stderr)
405        out_buf = ""
406        err_buf = ""
407        # Output from this loop iteration is written to func_stdout for later
408        # processing.
409        func_stdout = ""
410        if fork:
411            break
412        command_timed_out = False
413        if time_out is not None:
414            # Designate a SIGALRM handling function and set alarm.
415            signal.signal(signal.SIGALRM, shell_cmd_timed_out)
416            signal.alarm(time_out)
417        try:
418            if return_stderr:
419                for line in sub_proc.stderr:
420                    err_buf += line
421                    if not print_output:
422                        continue
423                    func_stdout += line
424            for line in sub_proc.stdout:
425                out_buf += line
426                if not print_output:
427                    continue
428                func_stdout += line
429        except IOError:
430            command_timed_out = True
431        sub_proc.communicate()
432        shell_rc = sub_proc.returncode
433        # Restore the original SIGALRM handler and clear the alarm.
434        signal.signal(signal.SIGALRM, original_sigalrm_handler)
435        signal.alarm(0)
436        if shell_rc in allowed_shell_rcs:
437            break
438        err_msg = "The prior shell command failed.\n"
439        if quiet:
440            err_msg += gp.sprint_var(command_string)
441        if command_timed_out:
442            err_msg += gp.sprint_var(command_timed_out)
443            err_msg += gp.sprint_var(time_out)
444            err_msg += gp.sprint_varx("child_pid", sub_proc.pid)
445        err_msg += gp.sprint_var(attempt_num)
446        err_msg += gp.sprint_var(shell_rc, 1)
447        err_msg += gp.sprint_var(allowed_shell_rcs, 1)
448        if not print_output:
449            if return_stderr:
450                err_msg += "err_buf:\n" + err_buf
451            err_msg += "out_buf:\n" + out_buf
452        if show_err:
453            if robot_env:
454                func_stdout += grp.sprint_error_report(err_msg)
455            else:
456                func_stdout += gp.sprint_error_report(err_msg)
457        func_history_stdout += func_stdout
458        if attempt_num < max_attempts:
459            func_history_stdout += gp.sprint_issuing("time.sleep("
460                                                     + str(retry_sleep_time)
461                                                     + ")")
462            time.sleep(retry_sleep_time)
463
464    if shell_rc not in allowed_shell_rcs:
465        func_stdout = func_history_stdout
466
467    if robot_env:
468        grp.rprint(func_stdout)
469    else:
470        sys.stdout.write(func_stdout)
471        sys.stdout.flush()
472
473    if shell_rc not in allowed_shell_rcs:
474        if not ignore_err:
475            if robot_env:
476                BuiltIn().fail(err_msg)
477            else:
478                raise ValueError("The prior shell command failed.\n")
479
480    if return_stderr:
481        return shell_rc, out_buf, err_buf
482    else:
483        return shell_rc, out_buf
484
485
486def t_shell_cmd(command_string, **kwargs):
487    r"""
488    Search upward in the the call stack to obtain the test_mode argument, add
489    it to kwargs and then call shell_cmd and return the result.
490
491    See shell_cmd prolog for details on all arguments.
492    """
493
494    if 'test_mode' in kwargs:
495        error_message = "Programmer error - test_mode is not a valid" +\
496            " argument to this function."
497        gp.print_error_report(error_message)
498        exit(1)
499
500    test_mode = gp.get_stack_var('test_mode',
501                                 int(gp.get_var_value(None, 0, "test_mode")))
502    kwargs['test_mode'] = test_mode
503
504    return shell_cmd(command_string, **kwargs)
505