xref: /openbmc/openbmc-test-automation/lib/gen_call_robot.py (revision 41e5ad25eb9d16be3f823222f94204b71fac8274)
1 #!/usr/bin/env python3
2 
3 r"""
4 This module provides functions which are useful to plug-ins call-point programs that wish to make external
5 robot program calls.
6 """
7 
8 import imp
9 import os
10 import re
11 import subprocess
12 import sys
13 import time
14 
15 import gen_cmd as gc
16 import gen_misc as gm
17 import gen_print as gp
18 import gen_valid as gv
19 
20 base_path = (
21     os.path.dirname(os.path.dirname(imp.find_module("gen_robot_print")[1]))
22     + os.sep
23 )
24 
25 
26 def init_robot_out_parms(extra_prefix=""):
27     r"""
28     Initialize robot output parms such as outputdir, output, etc.
29 
30     This function will set global values for the following robot output parms.
31 
32     outputdir, output, log, report, loglevel, consolecolors, consolemarkers
33 
34     This function would typically be called prior to calling create_robot_cmd_string.
35 
36     Description of argument(s):
37     extra_prefix                    An extra prefix to be appended to the default prefix for output file
38                                     names.
39     """
40 
41     gp.dprint_executing()
42     AUTOBOOT_OPENBMC_NICKNAME = gm.get_mod_global("AUTOBOOT_OPENBMC_NICKNAME")
43 
44     # Set values for call to create_robot_cmd_string.
45     # Environment variable TMP_ROBOT_DIR_PATH can be set by the user to indicate that robot-generated output
46     # should initially be written to the specified temporary directory and then moved to the normal output
47     # location after completion.
48     outputdir = os.environ.get(
49         "TMP_ROBOT_DIR_PATH",
50         os.environ.get(
51             "STATUS_DIR_PATH", os.environ.get("HOME", ".") + "/status"
52         ),
53     )
54     outputdir = gm.add_trailing_slash(outputdir)
55     seconds = time.time()
56     loc_time = time.localtime(seconds)
57     time_string = time.strftime("%y%m%d.%H%M%S", loc_time)
58     file_prefix = (
59         AUTOBOOT_OPENBMC_NICKNAME + "." + extra_prefix + time_string + "."
60     )
61     # Environment variable SAVE_STATUS_POLICY governs when robot-generated output files (e.g. the log.html)
62     # will be moved from TMP_ROBOT_DIR_PATH to FFDC_DIR_PATH.  Valid values are "ALWAYS", "NEVER" and "FAIL".
63     SAVE_STATUS_POLICY = os.environ.get("SAVE_STATUS_POLICY", "ALWAYS")
64     if SAVE_STATUS_POLICY == "NEVER":
65         output = "NONE"
66         log = "NONE"
67         report = "NONE"
68     else:
69         output = file_prefix + "output.xml"
70         log = file_prefix + "log.html"
71         report = file_prefix + "report.html"
72     loglevel = "TRACE"
73     consolecolors = "off"
74     consolemarkers = "off"
75 
76     # Make create_robot_cmd_string values global.
77     gm.set_mod_global(outputdir)
78     gm.set_mod_global(output)
79     gm.set_mod_global(log)
80     gm.set_mod_global(report)
81     gm.set_mod_global(loglevel)
82     gm.set_mod_global(consolecolors)
83     gm.set_mod_global(consolemarkers)
84 
85     return (
86         outputdir,
87         output,
88         log,
89         report,
90         loglevel,
91         consolecolors,
92         consolemarkers,
93     )
94 
95 
96 def init_robot_test_base_dir_path():
97     r"""
98     Initialize and validate the environment variable, ROBOT_TEST_BASE_DIR_PATH and set corresponding global
99     variable ROBOT_TEST_RUNNING_FROM_SB.
100 
101     If ROBOT_TEST_BASE_DIR_PATH is already set, this function will merely validate it.  This function will
102     also set environment variable ROBOT_TEST_RUNNING_FROM_SB when ROBOT_TEST_BASE_DIR_PATH is not pre-set.
103     """
104 
105     # ROBOT_TEST_BASE_DIR_PATH will be set as follows:
106     # This function will determine whether we are running in a user sandbox or from a standard apolloxxx
107     # environment.
108     # - User sandbox:
109     # If there is a <developer's home dir>/git/openbmc-test-automation/, ROBOT_TEST_BASE_DIR_PATH will be
110     # set to that path.  Otherwise, we set it to <program dir path>/git/openbmc-test-automation/
111     # - Not in user sandbox:
112     #   ROBOT_TEST_BASE_DIR_PATH will be set to <program dir path>/git/openbmc-test-automation/
113 
114     ROBOT_TEST_BASE_DIR_PATH = os.environ.get("ROBOT_TEST_BASE_DIR_PATH", "")
115     ROBOT_TEST_RUNNING_FROM_SB = int(
116         os.environ.get("ROBOT_TEST_RUNNING_FROM_SB", "0")
117     )
118     if ROBOT_TEST_BASE_DIR_PATH == "":
119         # ROBOT_TEST_BASE_DIR_PATH was not set by user/caller.
120         AUTOIPL_VERSION = os.environ.get("AUTOIPL_VERSION", "")
121         if AUTOIPL_VERSION == "":
122             ROBOT_TEST_BASE_DIR_PATH = base_path
123         else:
124             suffix = "git/openbmc-test-automation/"
125 
126             # Determine whether we're running out of a developer sandbox or simply out of an apolloxxx/bin
127             # path.
128             shell_rc, out_buf = gc.shell_cmd(
129                 "dirname $(which gen_print.py)",
130                 quiet=(not debug),
131                 print_output=0,
132             )
133             executable_base_dir_path = os.path.realpath(out_buf.rstrip()) + "/"
134             apollo_dir_path = (
135                 os.environ["AUTO_BASE_PATH"] + AUTOIPL_VERSION + "/bin/"
136             )
137             developer_home_dir_path = re.sub(
138                 "/sandbox.*", "", executable_base_dir_path
139             )
140             developer_home_dir_path = gm.add_trailing_slash(
141                 developer_home_dir_path
142             )
143             gp.dprint_vars(
144                 executable_base_dir_path,
145                 developer_home_dir_path,
146                 apollo_dir_path,
147             )
148 
149             ROBOT_TEST_RUNNING_FROM_SB = 0
150             if executable_base_dir_path != apollo_dir_path:
151                 ROBOT_TEST_RUNNING_FROM_SB = 1
152                 gp.dprint_vars(ROBOT_TEST_RUNNING_FROM_SB)
153                 ROBOT_TEST_BASE_DIR_PATH = developer_home_dir_path + suffix
154                 if not os.path.isdir(ROBOT_TEST_BASE_DIR_PATH):
155                     gp.dprint_timen(
156                         "NOTE: Sandbox directory "
157                         + ROBOT_TEST_BASE_DIR_PATH
158                         + " does not"
159                         + " exist."
160                     )
161                     # Fall back to the apollo dir path.
162                     ROBOT_TEST_BASE_DIR_PATH = apollo_dir_path + suffix
163             else:
164                 # Use to the apollo dir path.
165                 ROBOT_TEST_BASE_DIR_PATH = apollo_dir_path + suffix
166 
167     gv.valid_value(ROBOT_TEST_BASE_DIR_PATH)
168     gp.dprint_vars(
169         ROBOT_TEST_RUNNING_FROM_SB,
170         ROBOT_TEST_BASE_DIR_PATH,
171     )
172     gv.valid_dir_path(ROBOT_TEST_BASE_DIR_PATH)
173 
174     ROBOT_TEST_BASE_DIR_PATH = gm.add_trailing_slash(ROBOT_TEST_BASE_DIR_PATH)
175     gm.set_mod_global(ROBOT_TEST_BASE_DIR_PATH)
176     os.environ["ROBOT_TEST_BASE_DIR_PATH"] = ROBOT_TEST_BASE_DIR_PATH
177 
178     gm.set_mod_global(ROBOT_TEST_RUNNING_FROM_SB)
179     os.environ["ROBOT_TEST_RUNNING_FROM_SB"] = str(ROBOT_TEST_RUNNING_FROM_SB)
180 
181 
182 raw_robot_file_search_path = (
183     "${ROBOT_TEST_BASE_DIR_PATH}:"
184     + "${ROBOT_TEST_BASE_DIR_PATH}tests:${ROBOT_TEST_BASE_DIR_PATH}extended:"
185     + "${ROBOT_TEST_BASE_DIR_PATH}scratch:${PATH}"
186 )
187 
188 
189 def init_robot_file_path(robot_file_path):
190     r"""
191     Determine full path name for the file path passed in robot_file_path and return it.
192 
193     If robot_file_path contains a fully qualified path name, this function will verify that the file exists.
194     If robot_file_path contains a relative path, this function will search for the file and set
195     robot_file_path so that it contains the absolute path to the robot file.  This function will search for
196     the robot file using the raw_robot_file_search_path (defined above).  Note that if
197     ROBOT_TEST_BASE_DIR_PATH is not set, this function will call init_robot_test_base_dir_path to set it.
198 
199     Description of arguments:
200     robot_file_path                 The absolute or relative path to a robot file.
201     """
202 
203     gv.valid_value(robot_file_path)
204 
205     try:
206         if ROBOT_TEST_BASE_DIR_PATH is NONE:
207             init_robot_test_base_dir_path()
208     except NameError:
209         init_robot_test_base_dir_path()
210 
211     if not re.match(r".*\.(robot|py)$", robot_file_path):
212         # No suffix so we'll assign one of "\.robot".
213         robot_file_path = robot_file_path + ".robot"
214 
215     abs_path = 0
216     if robot_file_path[0:1] == "/":
217         abs_path = 1
218 
219     gp.dprint_vars(abs_path, robot_file_path)
220 
221     if not abs_path:
222         cmd_buf = 'echo -n "' + raw_robot_file_search_path + '"'
223         shell_rc, out_buf = gc.shell_cmd(
224             cmd_buf, quiet=(not debug), print_output=0
225         )
226         robot_file_search_paths = out_buf
227         gp.dprint_var(robot_file_search_paths)
228         robot_file_search_paths_list = robot_file_search_paths.split(":")
229         for search_path in robot_file_search_paths_list:
230             search_path = gm.add_trailing_slash(search_path)
231             candidate_file_path = search_path + robot_file_path
232             gp.dprint_var(candidate_file_path)
233             if os.path.isfile(candidate_file_path):
234                 gp.dprint_timen("Found full path to " + robot_file_path + ".")
235                 robot_file_path = candidate_file_path
236                 break
237 
238     gp.dprint_var(robot_file_path)
239     gv.valid_file_path(robot_file_path)
240 
241     return robot_file_path
242 
243 
244 def get_robot_parm_names():
245     r"""
246     Return a list containing all of the long parm names (e.g. --outputdir) supported by the robot program.
247     Double dashes are not included in the names returned.
248     """
249 
250     cmd_buf = (
251         "robot -h | egrep "
252         + "'^([ ]\\-[a-zA-Z0-9])?[ ]+--[a-zA-Z0-9]+[ ]+' | sed -re"
253         + " s'/.*\\-\\-//g' -e s'/ .*//g' | sort -u"
254     )
255     shell_rc, out_buf = gc.shell_cmd(cmd_buf, quiet=1, print_output=0)
256 
257     return out_buf.split("\n")
258 
259 
260 def create_robot_cmd_string(robot_file_path, *parms):
261     r"""
262     Create a robot command string and return it.  On failure, return an empty string.
263 
264     Description of arguments:
265     robot_file_path                 The path to the robot file to be run.
266     parms                           The list of parms to be included in the command string.  The name of each
267                                     variable in this list must be the same as the name of the corresponding
268                                     parm.  This function figures out that name.  This function is also able
269                                     to distinguish robot parms (e.g. --outputdir) from robot program parms
270                                     (all other parms which will be passed as "-v PARM_NAME:parm_value")..
271 
272     Example:
273 
274     The following call to this function...
275     cmd_buf = create_robot_cmd_string("tools/start_sol_console.robot", OPENBMC_HOST, quiet, test_mode, debug,
276     outputdir, output, log, report)
277 
278     Would return a string something like this.
279     robot -v OPENBMC_HOST:beye6 -v quiet:0 -v test_mode:1 -v debug:1
280     --outputdir=/gsa/ausgsa/projects/a/status --output=beye6.OS_Console.output.xml
281     --log=beye6.OS_Console.log.html --report=beye6.OS_Console.report.html tools/start_sol_console.robot
282     """
283 
284     robot_file_path = init_robot_file_path(robot_file_path)
285 
286     robot_parm_names = get_robot_parm_names()
287 
288     robot_parm_list = []
289 
290     stack_frame = 2
291     ix = 2
292     for arg in parms:
293         parm = arg
294         parm = gm.quote_bash_parm(gm.escape_bash_quotes(str(parm)))
295         var_name = gp.get_arg_name(None, ix, stack_frame)
296         if var_name in robot_parm_names:
297             p_string = "--" + var_name + "=" + str(parm)
298             robot_parm_list.append(p_string)
299         else:
300             p_string = "-v " + var_name + ":" + str(parm)
301             robot_parm_list.append(p_string)
302         ix += 1
303 
304     robot_cmd_buf = (
305         "robot " + " ".join(robot_parm_list) + " " + robot_file_path
306     )
307 
308     return robot_cmd_buf
309 
310 
311 # Global variables to aid in cleanup after running robot_cmd_fnc.
312 gcr_last_robot_cmd_buf = ""
313 gcr_last_robot_rc = 0
314 
315 
316 def process_robot_output_files(robot_cmd_buf=None, robot_rc=None, gzip=None):
317     r"""
318     Process robot output files which can involve several operations:
319     - If the files are in a temporary location, using SAVE_STATUS_POLICY to decide whether to move them to a
320       permanent location or to delete them.
321     - Gzipping them.
322 
323     Description of argument(s):
324     robot_cmd_buf                   The complete command string used to invoke robot.
325     robot_rc                        The return code from running the robot command string.
326     gzip                            Indicates whether robot-generated output should be gzipped.
327     """
328 
329     robot_cmd_buf = gm.dft(robot_cmd_buf, gcr_last_robot_cmd_buf)
330     robot_rc = gm.dft(robot_rc, gcr_last_robot_rc)
331     gzip = gm.dft(gzip, int(os.environ.get("GZIP_ROBOT", "1")))
332 
333     if robot_cmd_buf == "":
334         # This can legitimately occur if this function is called from an exit_function without the program
335         # having ever run robot_cmd_fnc.
336         return
337 
338     SAVE_STATUS_POLICY = os.environ.get("SAVE_STATUS_POLICY", "ALWAYS")
339     gp.qprint_vars(SAVE_STATUS_POLICY)
340 
341     # When SAVE_STATUS_POLICY is "NEVER" robot output files don't even get generated.
342     if SAVE_STATUS_POLICY == "NEVER":
343         return
344 
345     # Compose file_list based on robot command buffer passed in.
346     robot_cmd_buf_dict = gc.parse_command_string(robot_cmd_buf)
347     outputdir = robot_cmd_buf_dict["outputdir"]
348     outputdir = gm.add_trailing_slash(outputdir)
349     file_list = (
350         outputdir
351         + robot_cmd_buf_dict["output"]
352         + " "
353         + outputdir
354         + robot_cmd_buf_dict["log"]
355         + " "
356         + outputdir
357         + robot_cmd_buf_dict["report"]
358     )
359 
360     # Double checking that files are present.
361     shell_rc, out_buf = gc.shell_cmd(
362         "ls -1 " + file_list + " 2>/dev/null", show_err=0
363     )
364     file_list = re.sub("\n", " ", out_buf.rstrip("\n"))
365 
366     if file_list == "":
367         gp.qprint_timen(
368             "No robot output files were found in " + outputdir + "."
369         )
370         return
371     gp.qprint_var(robot_rc, gp.hexa())
372     if SAVE_STATUS_POLICY == "FAIL" and robot_rc == 0:
373         gp.qprint_timen(
374             "The call to robot produced no failures."
375             + "  Deleting robot output files."
376         )
377         gc.shell_cmd("rm -rf " + file_list)
378         return
379 
380     if gzip:
381         gc.shell_cmd("gzip -f " + file_list)
382         # Update the values in file_list.
383         file_list = re.sub(" ", ".gz ", file_list) + ".gz"
384 
385     # It TMP_ROBOT_DIR_PATH is set, it means the caller wanted the robot output initially directed to
386     # TMP_ROBOT_DIR_PATH but later moved to FFDC_DIR_PATH.  Otherwise, we're done.
387 
388     if os.environ.get("TMP_ROBOT_DIR_PATH", "") == "":
389         return
390 
391     # We're directing these to the FFDC dir path so that they'll be subjected to FFDC cleanup.
392     target_dir_path = os.environ.get(
393         "FFDC_DIR_PATH", os.environ.get("HOME", ".") + "/ffdc"
394     )
395     target_dir_path = gm.add_trailing_slash(target_dir_path)
396 
397     targ_file_list = [
398         re.sub(".*/", target_dir_path, x) for x in file_list.split(" ")
399     ]
400 
401     gc.shell_cmd(
402         "mv " + file_list + " " + target_dir_path + " >/dev/null", time_out=600
403     )
404 
405     gp.qprint_timen("New robot log file locations:")
406     gp.qprintn("\n".join(targ_file_list))
407 
408 
409 def robot_cmd_fnc(
410     robot_cmd_buf,
411     robot_jail=os.environ.get("ROBOT_JAIL", ""),
412     quiet=None,
413     test_mode=0,
414 ):
415     r"""
416     Run the robot command string.
417 
418     This function will set the various PATH variables correctly so that you are running the proper version of
419     all imported files, etc.
420 
421     Description of argument(s):
422     robot_cmd_buf                   The complete robot command string.
423     robot_jail                      Indicates that this is to run in "robot jail" meaning without visibility
424                                     to any apolloxxx import files, programs, etc.
425     test_mode                       If test_mode is set, this function will not actually run the command.
426     """
427 
428     quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0)))
429     gv.valid_value(robot_cmd_buf)
430 
431     # Set global variables to aid in cleanup with process_robot_output_files.
432     global gcr_last_robot_cmd_buf
433     global gcr_last_robot_rc
434     gcr_last_robot_cmd_buf = robot_cmd_buf
435 
436     # Get globals set by init_robot_test_base_dir_path().
437     module = sys.modules["__main__"]
438     try:
439         ROBOT_TEST_BASE_DIR_PATH = getattr(module, "ROBOT_TEST_BASE_DIR_PATH")
440     except NameError:
441         init_robot_test_base_dir_path()
442         ROBOT_TEST_BASE_DIR_PATH = getattr(module, "ROBOT_TEST_BASE_DIR_PATH")
443 
444     ROBOT_TEST_RUNNING_FROM_SB = gm.get_mod_global(
445         "ROBOT_TEST_RUNNING_FROM_SB"
446     )
447 
448     if robot_jail == "":
449         if ROBOT_TEST_RUNNING_FROM_SB:
450             robot_jail = 0
451         else:
452             robot_jail = 1
453 
454     robot_jail = int(robot_jail)
455     ROBOT_JAIL = os.environ.get("ROBOT_JAIL", "")
456     gp.dprint_vars(
457         ROBOT_TEST_BASE_DIR_PATH,
458         ROBOT_TEST_RUNNING_FROM_SB,
459         ROBOT_JAIL,
460         robot_jail,
461     )
462 
463     # Save PATH and PYTHONPATH to be restored later.
464     os.environ["SAVED_PYTHONPATH"] = os.environ.get("PYTHONPATH", "")
465     os.environ["SAVED_PATH"] = os.environ.get("PATH", "")
466 
467     if robot_jail:
468         # Make sure required programs like python and robot can be found in the new restricted PATH.
469         required_programs = "python robot"
470         # It is expected that there will be a "python" program in the tool base bin path which is really a
471         # link to select_version.  Ditto for "robot".  Call each with the --print_only option to get the
472         # paths to the "real" programs.
473         cmd_buf = (
474             "for program in "
475             + required_programs
476             + " ; do dirname $(${program} --print_only) ; done 2>/dev/null"
477         )
478         rc, out_buf = gc.shell_cmd(cmd_buf, quiet=1, print_output=0)
479         PYTHONPATH = ROBOT_TEST_BASE_DIR_PATH + "lib"
480         NEW_PATH_LIST = [ROBOT_TEST_BASE_DIR_PATH + "bin"]
481         NEW_PATH_LIST.extend(list(set(out_buf.rstrip("\n").split("\n"))))
482         NEW_PATH_LIST.extend(
483             [
484                 "/usr/local/sbin",
485                 "/usr/local/bin",
486                 "/usr/sbin",
487                 "/usr/bin",
488                 "/sbin",
489                 "/bin",
490             ]
491         )
492         PATH = ":".join(NEW_PATH_LIST)
493     else:
494         PYTHONPATH = (
495             os.environ.get("PYTHONPATH", "")
496             + ":"
497             + ROBOT_TEST_BASE_DIR_PATH
498             + "lib"
499         )
500         PATH = (
501             os.environ.get("PATH", "") + ":" + ROBOT_TEST_BASE_DIR_PATH + "bin"
502         )
503 
504     os.environ["PYTHONPATH"] = PYTHONPATH
505     os.environ["PATH"] = PATH
506     gp.dprint_vars(PATH, PYTHONPATH)
507 
508     os.environ["FFDC_DIR_PATH_STYLE"] = os.environ.get(
509         "FFDC_DIR_PATH_STYLE", "1"
510     )
511     gp.qpissuing(robot_cmd_buf, test_mode)
512     if test_mode:
513         os.environ["PATH"] = os.environ.get("SAVED_PATH", "")
514         os.environ["PYTHONPATH"] = os.environ.get("SAVED_PYTHONPATH", "")
515         return True
516 
517     if quiet:
518         DEVNULL = open(os.devnull, "wb")
519         stdout = DEVNULL
520     else:
521         stdout = None
522     sub_proc = subprocess.Popen(robot_cmd_buf, stdout=stdout, shell=True)
523     sub_proc.communicate()
524     shell_rc = sub_proc.returncode
525     os.environ["PATH"] = os.environ.get("SAVED_PATH", "")
526     os.environ["PYTHONPATH"] = os.environ.get("SAVED_PYTHONPATH", "")
527     gcr_last_robot_rc = shell_rc
528     process_robot_output_files()
529     if shell_rc != 0:
530         gp.print_var(shell_rc, gp.hexa())
531         return False
532 
533     return True
534