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