blob: d6ae1998e251b154bca7edb3767f2c1de74ed071 [file] [log] [blame]
Michael Walshc3b512e2017-02-20 15:59:01 -06001#!/usr/bin/env python
2
3r"""
4This module provides command execution functions such as cmd_fnc and cmd_fnc_u.
5"""
6
Michael Walsh21083d22018-06-01 14:19:32 -05007import os
Michael Walshc3b512e2017-02-20 15:59:01 -06008import sys
9import subprocess
Michael Walshf41fac82017-08-02 15:05:24 -050010import collections
Michael Walsh21083d22018-06-01 14:19:32 -050011import signal
12import time
Michael Walshc3b512e2017-02-20 15:59:01 -060013
Michael Walshc3b512e2017-02-20 15:59:01 -060014import gen_print as gp
15import gen_valid as gv
16import gen_misc as gm
Michael Walshd6901502017-11-14 12:58:37 -060017
18robot_env = gp.robot_env
19
Michael Walshafc53a22017-04-12 15:52:28 -050020if robot_env:
21 import gen_robot_print as grp
Michael Walsha3e2f532018-01-10 13:43:42 -060022 from robot.libraries.BuiltIn import BuiltIn
Michael Walshc3b512e2017-02-20 15:59:01 -060023
24
Michael Walsh21083d22018-06-01 14:19:32 -050025# cmd_fnc and cmd_fnc_u should now be considered deprecated. shell_cmd and
26# t_shell_cmd should be used instead.
Michael Walshc3b512e2017-02-20 15:59:01 -060027def cmd_fnc(cmd_buf,
28 quiet=None,
29 test_mode=None,
Michael Walshafc53a22017-04-12 15:52:28 -050030 debug=0,
Michael Walshc3b512e2017-02-20 15:59:01 -060031 print_output=1,
Michael Walshcfe9fed2017-09-12 17:13:10 -050032 show_err=1,
Michael Walsha3e2f532018-01-10 13:43:42 -060033 return_stderr=0,
34 ignore_err=1):
Michael Walshc3b512e2017-02-20 15:59:01 -060035 r"""
Michael Walshcfe9fed2017-09-12 17:13:10 -050036 Run the given command in a shell and return the shell return code and the
37 output.
Michael Walshc3b512e2017-02-20 15:59:01 -060038
39 Description of arguments:
40 cmd_buf The command string to be run in a shell.
41 quiet Indicates whether this function should run
Michael Walshcfe9fed2017-09-12 17:13:10 -050042 the print_issuing() function which prints
43 "Issuing: <cmd string>" to stdout.
Michael Walshc3b512e2017-02-20 15:59:01 -060044 test_mode If test_mode is set, this function will
Michael Walshcfe9fed2017-09-12 17:13:10 -050045 not actually run the command. If
46 print_output is set, it will print
47 "(test_mode) Issuing: <cmd string>" to
48 stdout.
Michael Walshc3b512e2017-02-20 15:59:01 -060049 debug If debug is set, this function will print
50 extra debug info.
51 print_output If this is set, this function will print
Michael Walshcfe9fed2017-09-12 17:13:10 -050052 the stdout/stderr generated by the shell
53 command.
Michael Walshc3b512e2017-02-20 15:59:01 -060054 show_err If show_err is set, this function will
Michael Walshcfe9fed2017-09-12 17:13:10 -050055 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.
Michael Walshc3b512e2017-02-20 15:59:01 -060062 """
63
Michael Walshcfe9fed2017-09-12 17:13:10 -050064 # Determine default values.
Michael Walshc3b512e2017-02-20 15:59:01 -060065 quiet = int(gm.global_default(quiet, 0))
66 test_mode = int(gm.global_default(test_mode, 0))
Michael Walshc3b512e2017-02-20 15:59:01 -060067
68 if debug:
Michael Walshafc53a22017-04-12 15:52:28 -050069 gp.print_vars(cmd_buf, quiet, test_mode, debug)
Michael Walshc3b512e2017-02-20 15:59:01 -060070
71 err_msg = gv.svalid_value(cmd_buf)
72 if err_msg != "":
73 raise ValueError(err_msg)
74
75 if not quiet:
Michael Walshafc53a22017-04-12 15:52:28 -050076 gp.pissuing(cmd_buf, test_mode)
Michael Walshc3b512e2017-02-20 15:59:01 -060077
78 if test_mode:
Michael Walshcfe9fed2017-09-12 17:13:10 -050079 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
Michael Walshc3b512e2017-02-20 15:59:01 -060089
90 sub_proc = subprocess.Popen(cmd_buf,
91 bufsize=1,
92 shell=True,
Michael Walsh3ba8ecd2018-04-24 11:33:25 -050093 executable='/bin/bash',
Michael Walshc3b512e2017-02-20 15:59:01 -060094 stdout=subprocess.PIPE,
Michael Walshcfe9fed2017-09-12 17:13:10 -050095 stderr=stderr)
Michael Walshc3b512e2017-02-20 15:59:01 -060096 out_buf = ""
Michael Walshcfe9fed2017-09-12 17:13:10 -050097 if return_stderr:
98 for line in sub_proc.stderr:
George Keishingc191ed72019-06-10 07:45:59 -050099 try:
100 err_buf += line
101 except TypeError:
102 line = line.decode("utf-8")
103 err_buf += line
Michael Walshcfe9fed2017-09-12 17:13:10 -0500104 if not print_output:
105 continue
George Keishingc191ed72019-06-10 07:45:59 -0500106 gp.gp_print(line)
Michael Walshc3b512e2017-02-20 15:59:01 -0600107 for line in sub_proc.stdout:
George Keishingc191ed72019-06-10 07:45:59 -0500108 try:
109 out_buf += line
110 except TypeError:
111 line = line.decode("utf-8")
112 out_buf += line
Michael Walshc3b512e2017-02-20 15:59:01 -0600113 if not print_output:
114 continue
George Keishingc191ed72019-06-10 07:45:59 -0500115 gp.gp_print(line)
Michael Walshc3b512e2017-02-20 15:59:01 -0600116 if print_output and not robot_env:
117 sys.stdout.flush()
118 sub_proc.communicate()
119 shell_rc = sub_proc.returncode
Michael Walsha3e2f532018-01-10 13:43:42 -0600120 if shell_rc != 0:
121 err_msg = "The prior shell command failed.\n"
122 err_msg += gp.sprint_var(shell_rc, 1)
Michael Walshcfe9fed2017-09-12 17:13:10 -0500123 if not print_output:
124 err_msg += "out_buf:\n" + out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600125
Michael Walsha3e2f532018-01-10 13:43:42 -0600126 if show_err:
127 if robot_env:
128 grp.rprint_error_report(err_msg)
129 else:
130 gp.print_error_report(err_msg)
131 if not ignore_err:
132 if robot_env:
133 BuiltIn().fail(err_msg)
134 else:
135 raise ValueError(err_msg)
Michael Walshcfe9fed2017-09-12 17:13:10 -0500136
137 if return_stderr:
138 return shell_rc, out_buf, err_buf
139 else:
140 return shell_rc, out_buf
Michael Walshc3b512e2017-02-20 15:59:01 -0600141
Michael Walshc3b512e2017-02-20 15:59:01 -0600142
Michael Walshc3b512e2017-02-20 15:59:01 -0600143def cmd_fnc_u(cmd_buf,
144 quiet=None,
145 debug=None,
146 print_output=1,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500147 show_err=1,
Michael Walsha3e2f532018-01-10 13:43:42 -0600148 return_stderr=0,
149 ignore_err=1):
Michael Walshc3b512e2017-02-20 15:59:01 -0600150 r"""
151 Call cmd_fnc with test_mode=0. See cmd_fnc (above) for details.
152
153 Note the "u" in "cmd_fnc_u" stands for "unconditional".
154 """
155
156 return cmd_fnc(cmd_buf, test_mode=0, quiet=quiet, debug=debug,
Michael Walshcfe9fed2017-09-12 17:13:10 -0500157 print_output=print_output, show_err=show_err,
Michael Walsha3e2f532018-01-10 13:43:42 -0600158 return_stderr=return_stderr, ignore_err=ignore_err)
Michael Walshc3b512e2017-02-20 15:59:01 -0600159
Michael Walshf41fac82017-08-02 15:05:24 -0500160
Michael Walshf41fac82017-08-02 15:05:24 -0500161def parse_command_string(command_string):
Michael Walshf41fac82017-08-02 15:05:24 -0500162 r"""
163 Parse a bash command-line command string and return the result as a
164 dictionary of parms.
165
166 This can be useful for answering questions like "What did the user specify
167 as the value for parm x in the command string?".
168
169 This function expects the command string to follow the following posix
170 conventions:
171 - Short parameters:
172 -<parm name><space><arg value>
173 - Long parameters:
174 --<parm name>=<arg value>
175
176 The first item in the string will be considered to be the command. All
177 values not conforming to the specifications above will be considered
178 positional parms. If there are multiple parms with the same name, they
179 will be put into a list (see illustration below where "-v" is specified
180 multiple times).
181
182 Description of argument(s):
183 command_string The complete command string including all
184 parameters and arguments.
185
186 Sample input:
187
188 robot_cmd_buf: robot -v
189 OPENBMC_HOST:dummy1 -v keyword_string:'Set Auto Reboot no' -v
190 lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot -v
191 quiet:0 -v test_mode:0 -v debug:0
192 --outputdir='/home/user1/status/children/'
193 --output=dummy1.Auto_reboot.170802.124544.output.xml
194 --log=dummy1.Auto_reboot.170802.124544.log.html
195 --report=dummy1.Auto_reboot.170802.124544.report.html
196 /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
197
198 Sample output:
199
200 robot_cmd_buf_dict:
201 robot_cmd_buf_dict[command]: robot
202 robot_cmd_buf_dict[v]:
203 robot_cmd_buf_dict[v][0]: OPENBMC_HOST:dummy1
204 robot_cmd_buf_dict[v][1]: keyword_string:Set Auto
205 Reboot no
206 robot_cmd_buf_dict[v][2]:
207 lib_file_path:/home/user1/git/openbmc-test-automation/lib/utils.robot
208 robot_cmd_buf_dict[v][3]: quiet:0
209 robot_cmd_buf_dict[v][4]: test_mode:0
210 robot_cmd_buf_dict[v][5]: debug:0
211 robot_cmd_buf_dict[outputdir]:
212 /home/user1/status/children/
213 robot_cmd_buf_dict[output]:
214 dummy1.Auto_reboot.170802.124544.output.xml
215 robot_cmd_buf_dict[log]:
216 dummy1.Auto_reboot.170802.124544.log.html
217 robot_cmd_buf_dict[report]:
218 dummy1.Auto_reboot.170802.124544.report.html
219 robot_cmd_buf_dict[positional]:
220 /home/user1/git/openbmc-test-automation/extended/run_keyword.robot
221 """
222
223 # We want the parms in the string broken down the way bash would do it,
224 # so we'll call upon bash to do that by creating a simple inline bash
225 # function.
226 bash_func_def = "function parse { for parm in \"${@}\" ; do" +\
227 " echo $parm ; done ; }"
228
229 rc, outbuf = cmd_fnc_u(bash_func_def + " ; parse " + command_string,
230 quiet=1, print_output=0)
231 command_string_list = outbuf.rstrip("\n").split("\n")
232
233 command_string_dict = collections.OrderedDict()
234 ix = 1
235 command_string_dict['command'] = command_string_list[0]
236 while ix < len(command_string_list):
237 if command_string_list[ix].startswith("--"):
238 key, value = command_string_list[ix].split("=")
239 key = key.lstrip("-")
240 elif command_string_list[ix].startswith("-"):
241 key = command_string_list[ix].lstrip("-")
242 ix += 1
243 try:
244 value = command_string_list[ix]
245 except IndexError:
246 value = ""
247 else:
248 key = 'positional'
249 value = command_string_list[ix]
250 if key in command_string_dict:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500251 if isinstance(command_string_dict[key], str):
Michael Walshf41fac82017-08-02 15:05:24 -0500252 command_string_dict[key] = [command_string_dict[key]]
253 command_string_dict[key].append(value)
254 else:
255 command_string_dict[key] = value
256 ix += 1
257
258 return command_string_dict
Michael Walsh21083d22018-06-01 14:19:32 -0500259
260
261# Save the original SIGALRM handler for later restoration by shell_cmd.
262original_sigalrm_handler = signal.getsignal(signal.SIGALRM)
263
264
265def shell_cmd_timed_out(signal_number,
266 frame):
267 r"""
268 Handle an alarm signal generated during the shell_cmd function.
269 """
270
271 gp.dprint_executing()
272 # Get subprocess pid from shell_cmd's call stack.
273 sub_proc = gp.get_stack_var('sub_proc', 0)
274 pid = sub_proc.pid
275 # Terminate the child process.
276 os.kill(pid, signal.SIGTERM)
277 # Restore the original SIGALRM handler.
278 signal.signal(signal.SIGALRM, original_sigalrm_handler)
279
280 return
281
282
283def shell_cmd(command_string,
284 quiet=None,
Michael Walsh55abd1b2018-08-31 13:00:50 -0500285 print_output=None,
Michael Walsh21083d22018-06-01 14:19:32 -0500286 show_err=1,
287 test_mode=0,
288 time_out=None,
289 max_attempts=1,
290 retry_sleep_time=5,
291 allowed_shell_rcs=[0],
292 ignore_err=None,
Michael Walshfaafa9c2018-06-27 16:39:31 -0500293 return_stderr=0,
294 fork=0):
Michael Walsh21083d22018-06-01 14:19:32 -0500295 r"""
296 Run the given command string in a shell and return a tuple consisting of
297 the shell return code and the output.
298
299 Description of argument(s):
300 command_string The command string to be run in a shell
301 (e.g. "ls /tmp").
302 quiet If set to 0, this function will print
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500303 "Issuing: <cmd string>" to stdout. When
304 the quiet argument is set to None, this
305 function will assign a default value by
306 searching upward in the stack for the
307 quiet variable value. If no such value is
308 found, quiet is set to 0.
Michael Walsh21083d22018-06-01 14:19:32 -0500309 print_output If this is set, this function will print
310 the stdout/stderr generated by the shell
311 command to stdout.
312 show_err If show_err is set, this function will
313 print a standardized error report if the
314 shell command fails (i.e. if the shell
315 command returns a shell_rc that is not in
316 allowed_shell_rcs). Note: Error text is
317 only printed if ALL attempts to run the
318 command_string fail. In other words, if
319 the command execution is ultimately
320 successful, initial failures are hidden.
321 test_mode If test_mode is set, this function will
322 not actually run the command. If
323 print_output is also set, this function
324 will print "(test_mode) Issuing: <cmd
325 string>" to stdout. A caller should call
326 shell_cmd directly if they wish to have
327 the command string run unconditionally.
328 They should call the t_shell_cmd wrapper
329 (defined below) if they wish to run the
330 command string only if the prevailing
331 test_mode variable is set to 0.
332 time_out A time-out value expressed in seconds. If
333 the command string has not finished
334 executing within <time_out> seconds, it
335 will be halted and counted as an error.
336 max_attempts The max number of attempts that should be
337 made to run the command string.
338 retry_sleep_time The number of seconds to sleep between
339 attempts.
340 allowed_shell_rcs A list of integers indicating which
341 shell_rc values are not to be considered
342 errors.
343 ignore_err Ignore error means that a failure
344 encountered by running the command string
345 will not be raised as a python exception.
346 When the ignore_err argument is set to
347 None, this function will assign a default
348 value by searching upward in the stack for
349 the ignore_err variable value. If no such
350 value is found, ignore_err is set to 1.
351 return_stderr If return_stderr is set, this function
352 will process the stdout and stderr streams
353 from the shell command separately. In
354 such a case, the tuple returned by this
355 function will consist of three values
356 rather than just two: rc, stdout, stderr.
Michael Walshfaafa9c2018-06-27 16:39:31 -0500357 fork Run the command string asynchronously
358 (i.e. don't wait for status of the child
359 process and don't try to get
360 stdout/stderr).
Michael Walsh21083d22018-06-01 14:19:32 -0500361 """
362
363 # Assign default values to some of the arguments to this function.
Michael Walsh4cb9b2a2018-06-08 17:57:00 -0500364 quiet = int(gm.dft(quiet, gp.get_stack_var('quiet', 0)))
Michael Walsh55abd1b2018-08-31 13:00:50 -0500365 print_output = int(gm.dft(print_output, not quiet))
Michael Walsh45fead42018-09-26 17:20:48 -0500366 show_err = int(show_err)
367 ignore_err = int(gm.dft(ignore_err,
368 gp.get_stack_var('ignore_err',
369 gp.get_var_value(ignore_err, 1))))
Michael Walsh21083d22018-06-01 14:19:32 -0500370
371 err_msg = gv.svalid_value(command_string)
372 if err_msg != "":
373 raise ValueError(err_msg)
374
375 if not quiet:
376 gp.print_issuing(command_string, test_mode)
377
378 if test_mode:
379 if return_stderr:
380 return 0, "", ""
381 else:
382 return 0, ""
383
384 # Convert each list entry to a signed value.
385 allowed_shell_rcs = [gm.to_signed(x) for x in allowed_shell_rcs]
386
387 if return_stderr:
388 stderr = subprocess.PIPE
389 else:
390 stderr = subprocess.STDOUT
391
392 shell_rc = 0
393 out_buf = ""
394 err_buf = ""
395 # Write all output to func_history_stdout rather than directly to stdout.
396 # This allows us to decide what to print after all attempts to run the
397 # command string have been made. func_history_stdout will contain the
398 # complete stdout history from the current invocation of this function.
399 func_history_stdout = ""
400 for attempt_num in range(1, max_attempts + 1):
401 sub_proc = subprocess.Popen(command_string,
402 bufsize=1,
403 shell=True,
404 executable='/bin/bash',
405 stdout=subprocess.PIPE,
406 stderr=stderr)
407 out_buf = ""
408 err_buf = ""
409 # Output from this loop iteration is written to func_stdout for later
410 # processing.
411 func_stdout = ""
Michael Walshfaafa9c2018-06-27 16:39:31 -0500412 if fork:
413 break
Michael Walsh21083d22018-06-01 14:19:32 -0500414 command_timed_out = False
415 if time_out is not None:
416 # Designate a SIGALRM handling function and set alarm.
417 signal.signal(signal.SIGALRM, shell_cmd_timed_out)
418 signal.alarm(time_out)
419 try:
420 if return_stderr:
421 for line in sub_proc.stderr:
George Keishingc191ed72019-06-10 07:45:59 -0500422 try:
423 err_buf += line
424 except TypeError:
425 line = line.decode("utf-8")
426 err_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500427 if not print_output:
428 continue
429 func_stdout += line
430 for line in sub_proc.stdout:
George Keishingc191ed72019-06-10 07:45:59 -0500431 try:
432 out_buf += line
433 except TypeError:
434 line = line.decode("utf-8")
435 out_buf += line
Michael Walsh21083d22018-06-01 14:19:32 -0500436 if not print_output:
437 continue
438 func_stdout += line
439 except IOError:
440 command_timed_out = True
441 sub_proc.communicate()
442 shell_rc = sub_proc.returncode
443 # Restore the original SIGALRM handler and clear the alarm.
444 signal.signal(signal.SIGALRM, original_sigalrm_handler)
445 signal.alarm(0)
446 if shell_rc in allowed_shell_rcs:
447 break
448 err_msg = "The prior shell command failed.\n"
Michael Walsha750ed72018-07-24 16:18:09 -0500449 if quiet:
450 err_msg += gp.sprint_var(command_string)
Michael Walsh21083d22018-06-01 14:19:32 -0500451 if command_timed_out:
452 err_msg += gp.sprint_var(command_timed_out)
453 err_msg += gp.sprint_var(time_out)
454 err_msg += gp.sprint_varx("child_pid", sub_proc.pid)
455 err_msg += gp.sprint_var(attempt_num)
456 err_msg += gp.sprint_var(shell_rc, 1)
457 err_msg += gp.sprint_var(allowed_shell_rcs, 1)
458 if not print_output:
459 if return_stderr:
460 err_msg += "err_buf:\n" + err_buf
461 err_msg += "out_buf:\n" + out_buf
462 if show_err:
463 if robot_env:
464 func_stdout += grp.sprint_error_report(err_msg)
465 else:
466 func_stdout += gp.sprint_error_report(err_msg)
467 func_history_stdout += func_stdout
468 if attempt_num < max_attempts:
Joy Onyerikwu004ad3c2018-06-11 16:29:56 -0500469 func_history_stdout += gp.sprint_issuing("time.sleep("
470 + str(retry_sleep_time)
471 + ")")
Michael Walsh21083d22018-06-01 14:19:32 -0500472 time.sleep(retry_sleep_time)
473
474 if shell_rc not in allowed_shell_rcs:
475 func_stdout = func_history_stdout
476
477 if robot_env:
478 grp.rprint(func_stdout)
479 else:
480 sys.stdout.write(func_stdout)
481 sys.stdout.flush()
482
483 if shell_rc not in allowed_shell_rcs:
484 if not ignore_err:
485 if robot_env:
486 BuiltIn().fail(err_msg)
487 else:
488 raise ValueError("The prior shell command failed.\n")
489
490 if return_stderr:
491 return shell_rc, out_buf, err_buf
492 else:
493 return shell_rc, out_buf
494
495
496def t_shell_cmd(command_string, **kwargs):
497 r"""
498 Search upward in the the call stack to obtain the test_mode argument, add
499 it to kwargs and then call shell_cmd and return the result.
500
501 See shell_cmd prolog for details on all arguments.
502 """
503
504 if 'test_mode' in kwargs:
505 error_message = "Programmer error - test_mode is not a valid" +\
506 " argument to this function."
507 gp.print_error_report(error_message)
508 exit(1)
509
510 test_mode = gp.get_stack_var('test_mode',
511 int(gp.get_var_value(None, 0, "test_mode")))
512 kwargs['test_mode'] = test_mode
513
514 return shell_cmd(command_string, **kwargs)