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 imp
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(os.path.dirname(imp.find_module("gen_robot_print")[1]))
22    + os.sep
23)
24
25
26def 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
96def 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    OBMC_TOOLS_BASE_DIR_PATH = (
168        os.path.dirname(ROBOT_TEST_BASE_DIR_PATH.rstrip("/"))
169        + "/openbmc-tools/"
170    )
171    OPENBMCTOOL_DIR_PATH = OBMC_TOOLS_BASE_DIR_PATH + "openbmctool/"
172    JSON_CHECKER_TOOLS_DIR_PATH = (
173        OBMC_TOOLS_BASE_DIR_PATH + "expectedJsonChecker/"
174    )
175
176    gv.valid_value(ROBOT_TEST_BASE_DIR_PATH)
177    gp.dprint_vars(
178        ROBOT_TEST_RUNNING_FROM_SB,
179        ROBOT_TEST_BASE_DIR_PATH,
180        OBMC_TOOLS_BASE_DIR_PATH,
181        OPENBMCTOOL_DIR_PATH,
182        JSON_CHECKER_TOOLS_DIR_PATH,
183    )
184    gv.valid_dir_path(ROBOT_TEST_BASE_DIR_PATH)
185
186    ROBOT_TEST_BASE_DIR_PATH = gm.add_trailing_slash(ROBOT_TEST_BASE_DIR_PATH)
187    gm.set_mod_global(ROBOT_TEST_BASE_DIR_PATH)
188    os.environ["ROBOT_TEST_BASE_DIR_PATH"] = ROBOT_TEST_BASE_DIR_PATH
189
190    gm.set_mod_global(ROBOT_TEST_RUNNING_FROM_SB)
191    os.environ["ROBOT_TEST_RUNNING_FROM_SB"] = str(ROBOT_TEST_RUNNING_FROM_SB)
192
193    gm.set_mod_global(OBMC_TOOLS_BASE_DIR_PATH)
194    os.environ["OBMC_TOOLS_BASE_DIR_PATH"] = str(OBMC_TOOLS_BASE_DIR_PATH)
195
196    gm.set_mod_global(OPENBMCTOOL_DIR_PATH)
197    os.environ["OPENBMCTOOL_DIR_PATH"] = str(OPENBMCTOOL_DIR_PATH)
198
199    gm.set_mod_global(JSON_CHECKER_TOOLS_DIR_PATH)
200    os.environ["JSON_CHECKER_TOOLS_DIR_PATH"] = str(
201        JSON_CHECKER_TOOLS_DIR_PATH
202    )
203
204
205raw_robot_file_search_path = (
206    "${ROBOT_TEST_BASE_DIR_PATH}:"
207    + "${ROBOT_TEST_BASE_DIR_PATH}tests:${ROBOT_TEST_BASE_DIR_PATH}extended:"
208    + "${ROBOT_TEST_BASE_DIR_PATH}scratch:${PATH}"
209)
210
211
212def init_robot_file_path(robot_file_path):
213    r"""
214    Determine full path name for the file path passed in robot_file_path and return it.
215
216    If robot_file_path contains a fully qualified path name, this function will verify that the file exists.
217    If robot_file_path contains a relative path, this function will search for the file and set
218    robot_file_path so that it contains the absolute path to the robot file.  This function will search for
219    the robot file using the raw_robot_file_search_path (defined above).  Note that if
220    ROBOT_TEST_BASE_DIR_PATH is not set, this function will call init_robot_test_base_dir_path to set it.
221
222    Description of arguments:
223    robot_file_path                 The absolute or relative path to a robot file.
224    """
225
226    gv.valid_value(robot_file_path)
227
228    try:
229        if ROBOT_TEST_BASE_DIR_PATH is NONE:
230            init_robot_test_base_dir_path()
231    except NameError:
232        init_robot_test_base_dir_path()
233
234    if not re.match(r".*\.(robot|py)$", robot_file_path):
235        # No suffix so we'll assign one of "\.robot".
236        robot_file_path = robot_file_path + ".robot"
237
238    abs_path = 0
239    if robot_file_path[0:1] == "/":
240        abs_path = 1
241
242    gp.dprint_vars(abs_path, robot_file_path)
243
244    if not abs_path:
245        cmd_buf = 'echo -n "' + raw_robot_file_search_path + '"'
246        shell_rc, out_buf = gc.shell_cmd(
247            cmd_buf, quiet=(not debug), print_output=0
248        )
249        robot_file_search_paths = out_buf
250        gp.dprint_var(robot_file_search_paths)
251        robot_file_search_paths_list = robot_file_search_paths.split(":")
252        for search_path in robot_file_search_paths_list:
253            search_path = gm.add_trailing_slash(search_path)
254            candidate_file_path = search_path + robot_file_path
255            gp.dprint_var(candidate_file_path)
256            if os.path.isfile(candidate_file_path):
257                gp.dprint_timen("Found full path to " + robot_file_path + ".")
258                robot_file_path = candidate_file_path
259                break
260
261    gp.dprint_var(robot_file_path)
262    gv.valid_file_path(robot_file_path)
263
264    return robot_file_path
265
266
267def get_robot_parm_names():
268    r"""
269    Return a list containing all of the long parm names (e.g. --outputdir) supported by the robot program.
270    Double dashes are not included in the names returned.
271    """
272
273    cmd_buf = (
274        "robot -h | egrep "
275        + "'^([ ]\\-[a-zA-Z0-9])?[ ]+--[a-zA-Z0-9]+[ ]+' | sed -re"
276        + " s'/.*\\-\\-//g' -e s'/ .*//g' | sort -u"
277    )
278    shell_rc, out_buf = gc.shell_cmd(cmd_buf, quiet=1, print_output=0)
279
280    return out_buf.split("\n")
281
282
283def create_robot_cmd_string(robot_file_path, *parms):
284    r"""
285    Create a robot command string and return it.  On failure, return an empty string.
286
287    Description of arguments:
288    robot_file_path                 The path to the robot file to be run.
289    parms                           The list of parms to be included in the command string.  The name of each
290                                    variable in this list must be the same as the name of the corresponding
291                                    parm.  This function figures out that name.  This function is also able
292                                    to distinguish robot parms (e.g. --outputdir) from robot program parms
293                                    (all other parms which will be passed as "-v PARM_NAME:parm_value")..
294
295    Example:
296
297    The following call to this function...
298    cmd_buf = create_robot_cmd_string("tools/start_sol_console.robot", OPENBMC_HOST, quiet, test_mode, debug,
299    outputdir, output, log, report)
300
301    Would return a string something like this.
302    robot -v OPENBMC_HOST:beye6 -v quiet:0 -v test_mode:1 -v debug:1
303    --outputdir=/gsa/ausgsa/projects/a/status --output=beye6.OS_Console.output.xml
304    --log=beye6.OS_Console.log.html --report=beye6.OS_Console.report.html tools/start_sol_console.robot
305    """
306
307    robot_file_path = init_robot_file_path(robot_file_path)
308
309    robot_parm_names = get_robot_parm_names()
310
311    robot_parm_list = []
312
313    stack_frame = 2
314    ix = 2
315    for arg in parms:
316        parm = arg
317        parm = gm.quote_bash_parm(gm.escape_bash_quotes(str(parm)))
318        var_name = gp.get_arg_name(None, ix, stack_frame)
319        if var_name in robot_parm_names:
320            p_string = "--" + var_name + "=" + str(parm)
321            robot_parm_list.append(p_string)
322        else:
323            p_string = "-v " + var_name + ":" + str(parm)
324            robot_parm_list.append(p_string)
325        ix += 1
326
327    robot_cmd_buf = (
328        "robot " + " ".join(robot_parm_list) + " " + robot_file_path
329    )
330
331    return robot_cmd_buf
332
333
334# Global variables to aid in cleanup after running robot_cmd_fnc.
335gcr_last_robot_cmd_buf = ""
336gcr_last_robot_rc = 0
337
338
339def process_robot_output_files(robot_cmd_buf=None, robot_rc=None, gzip=None):
340    r"""
341    Process robot output files which can involve several operations:
342    - If the files are in a temporary location, using SAVE_STATUS_POLICY to decide whether to move them to a
343      permanent location or to delete them.
344    - Gzipping them.
345
346    Description of argument(s):
347    robot_cmd_buf                   The complete command string used to invoke robot.
348    robot_rc                        The return code from running the robot command string.
349    gzip                            Indicates whether robot-generated output should be gzipped.
350    """
351
352    robot_cmd_buf = gm.dft(robot_cmd_buf, gcr_last_robot_cmd_buf)
353    robot_rc = gm.dft(robot_rc, gcr_last_robot_rc)
354    gzip = gm.dft(gzip, int(os.environ.get("GZIP_ROBOT", "1")))
355
356    if robot_cmd_buf == "":
357        # This can legitimately occur if this function is called from an exit_function without the program
358        # having ever run robot_cmd_fnc.
359        return
360
361    SAVE_STATUS_POLICY = os.environ.get("SAVE_STATUS_POLICY", "ALWAYS")
362    gp.qprint_vars(SAVE_STATUS_POLICY)
363
364    # When SAVE_STATUS_POLICY is "NEVER" robot output files don't even get generated.
365    if SAVE_STATUS_POLICY == "NEVER":
366        return
367
368    # Compose file_list based on robot command buffer passed in.
369    robot_cmd_buf_dict = gc.parse_command_string(robot_cmd_buf)
370    outputdir = robot_cmd_buf_dict["outputdir"]
371    outputdir = gm.add_trailing_slash(outputdir)
372    file_list = (
373        outputdir
374        + robot_cmd_buf_dict["output"]
375        + " "
376        + outputdir
377        + robot_cmd_buf_dict["log"]
378        + " "
379        + outputdir
380        + robot_cmd_buf_dict["report"]
381    )
382
383    # Double checking that files are present.
384    shell_rc, out_buf = gc.shell_cmd(
385        "ls -1 " + file_list + " 2>/dev/null", show_err=0
386    )
387    file_list = re.sub("\n", " ", out_buf.rstrip("\n"))
388
389    if file_list == "":
390        gp.qprint_timen(
391            "No robot output files were found in " + outputdir + "."
392        )
393        return
394    gp.qprint_var(robot_rc, gp.hexa())
395    if SAVE_STATUS_POLICY == "FAIL" and robot_rc == 0:
396        gp.qprint_timen(
397            "The call to robot produced no failures."
398            + "  Deleting robot output files."
399        )
400        gc.shell_cmd("rm -rf " + file_list)
401        return
402
403    if gzip:
404        gc.shell_cmd("gzip -f " + file_list)
405        # Update the values in file_list.
406        file_list = re.sub(" ", ".gz ", file_list) + ".gz"
407
408    # It TMP_ROBOT_DIR_PATH is set, it means the caller wanted the robot output initially directed to
409    # TMP_ROBOT_DIR_PATH but later moved to FFDC_DIR_PATH.  Otherwise, we're done.
410
411    if os.environ.get("TMP_ROBOT_DIR_PATH", "") == "":
412        return
413
414    # We're directing these to the FFDC dir path so that they'll be subjected to FFDC cleanup.
415    target_dir_path = os.environ.get(
416        "FFDC_DIR_PATH", os.environ.get("HOME", ".") + "/ffdc"
417    )
418    target_dir_path = gm.add_trailing_slash(target_dir_path)
419
420    targ_file_list = [
421        re.sub(".*/", target_dir_path, x) for x in file_list.split(" ")
422    ]
423
424    gc.shell_cmd(
425        "mv " + file_list + " " + target_dir_path + " >/dev/null", time_out=600
426    )
427
428    gp.qprint_timen("New robot log file locations:")
429    gp.qprintn("\n".join(targ_file_list))
430
431
432def robot_cmd_fnc(
433    robot_cmd_buf,
434    robot_jail=os.environ.get("ROBOT_JAIL", ""),
435    quiet=None,
436    test_mode=0,
437):
438    r"""
439    Run the robot command string.
440
441    This function will set the various PATH variables correctly so that you are running the proper version of
442    all imported files, etc.
443
444    Description of argument(s):
445    robot_cmd_buf                   The complete robot command string.
446    robot_jail                      Indicates that this is to run in "robot jail" meaning without visibility
447                                    to any apolloxxx import files, programs, etc.
448    test_mode                       If test_mode is set, this function will not actually run the command.
449    """
450
451    quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0)))
452    gv.valid_value(robot_cmd_buf)
453
454    # Set global variables to aid in cleanup with process_robot_output_files.
455    global gcr_last_robot_cmd_buf
456    global gcr_last_robot_rc
457    gcr_last_robot_cmd_buf = robot_cmd_buf
458
459    # Get globals set by init_robot_test_base_dir_path().
460    module = sys.modules["__main__"]
461    try:
462        ROBOT_TEST_BASE_DIR_PATH = getattr(module, "ROBOT_TEST_BASE_DIR_PATH")
463    except NameError:
464        init_robot_test_base_dir_path()
465        ROBOT_TEST_BASE_DIR_PATH = getattr(module, "ROBOT_TEST_BASE_DIR_PATH")
466
467    ROBOT_TEST_RUNNING_FROM_SB = gm.get_mod_global(
468        "ROBOT_TEST_RUNNING_FROM_SB"
469    )
470    OPENBMCTOOL_DIR_PATH = gm.get_mod_global("OPENBMCTOOL_DIR_PATH")
471
472    if robot_jail == "":
473        if ROBOT_TEST_RUNNING_FROM_SB:
474            robot_jail = 0
475        else:
476            robot_jail = 1
477
478    robot_jail = int(robot_jail)
479    ROBOT_JAIL = os.environ.get("ROBOT_JAIL", "")
480    gp.dprint_vars(
481        ROBOT_TEST_BASE_DIR_PATH,
482        ROBOT_TEST_RUNNING_FROM_SB,
483        ROBOT_JAIL,
484        robot_jail,
485    )
486
487    # Save PATH and PYTHONPATH to be restored later.
488    os.environ["SAVED_PYTHONPATH"] = os.environ.get("PYTHONPATH", "")
489    os.environ["SAVED_PATH"] = os.environ.get("PATH", "")
490
491    if robot_jail:
492        # Make sure required programs like python and robot can be found in the new restricted PATH.
493        required_programs = "python robot"
494        # It is expected that there will be a "python" program in the tool base bin path which is really a
495        # link to select_version.  Ditto for "robot".  Call each with the --print_only option to get the
496        # paths to the "real" programs.
497        cmd_buf = (
498            "for program in "
499            + required_programs
500            + " ; do dirname $(${program} --print_only) ; done 2>/dev/null"
501        )
502        rc, out_buf = gc.shell_cmd(cmd_buf, quiet=1, print_output=0)
503        PYTHONPATH = ROBOT_TEST_BASE_DIR_PATH + "lib"
504        NEW_PATH_LIST = [ROBOT_TEST_BASE_DIR_PATH + "bin"]
505        NEW_PATH_LIST.extend(list(set(out_buf.rstrip("\n").split("\n"))))
506        NEW_PATH_LIST.extend(
507            [
508                "/usr/local/sbin",
509                "/usr/local/bin",
510                "/usr/sbin",
511                "/usr/bin",
512                "/sbin",
513                "/bin",
514                OPENBMCTOOL_DIR_PATH.rstrip("/"),
515            ]
516        )
517        PATH = ":".join(NEW_PATH_LIST)
518    else:
519        PYTHONPATH = (
520            os.environ.get("PYTHONPATH", "")
521            + ":"
522            + ROBOT_TEST_BASE_DIR_PATH
523            + "lib"
524        )
525        PATH = (
526            os.environ.get("PATH", "")
527            + ":"
528            + ROBOT_TEST_BASE_DIR_PATH
529            + "bin"
530            + ":"
531            + OPENBMCTOOL_DIR_PATH.rstrip("/")
532        )
533
534    os.environ["PYTHONPATH"] = PYTHONPATH
535    os.environ["PATH"] = PATH
536    gp.dprint_vars(PATH, PYTHONPATH)
537
538    os.environ["FFDC_DIR_PATH_STYLE"] = os.environ.get(
539        "FFDC_DIR_PATH_STYLE", "1"
540    )
541    gp.qpissuing(robot_cmd_buf, test_mode)
542    if test_mode:
543        os.environ["PATH"] = os.environ.get("SAVED_PATH", "")
544        os.environ["PYTHONPATH"] = os.environ.get("SAVED_PYTHONPATH", "")
545        return True
546
547    if quiet:
548        DEVNULL = open(os.devnull, "wb")
549        stdout = DEVNULL
550    else:
551        stdout = None
552    sub_proc = subprocess.Popen(robot_cmd_buf, stdout=stdout, shell=True)
553    sub_proc.communicate()
554    shell_rc = sub_proc.returncode
555    os.environ["PATH"] = os.environ.get("SAVED_PATH", "")
556    os.environ["PYTHONPATH"] = os.environ.get("SAVED_PYTHONPATH", "")
557    gcr_last_robot_rc = shell_rc
558    process_robot_output_files()
559    if shell_rc != 0:
560        gp.print_var(shell_rc, gp.hexa())
561        return False
562
563    return True
564