| #!/usr/bin/env python3 | 
 |  | 
 | r""" | 
 | This module has functions to support various data structures such as the boot_table, valid_boot_list and | 
 | boot_results_table. | 
 | """ | 
 |  | 
 | import glob | 
 | import json | 
 | import os | 
 | import tempfile | 
 |  | 
 | from robot.libraries.BuiltIn import BuiltIn | 
 | from tally_sheet import * | 
 |  | 
 | try: | 
 |     from robot.utils import DotDict | 
 | except ImportError: | 
 |     import collections | 
 |  | 
 | import gen_cmd as gc | 
 | import gen_misc as gm | 
 | import gen_print as gp | 
 | import gen_valid as gv | 
 | import var_funcs as vf | 
 |  | 
 | # The code base directory will be one level up from the directory containing this module. | 
 | code_base_dir_path = os.path.dirname(os.path.dirname(__file__)) + os.sep | 
 |  | 
 | redfish_support_trans_state = int( | 
 |     os.environ.get("REDFISH_SUPPORT_TRANS_STATE", 0) | 
 | ) or int( | 
 |     BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0) | 
 | ) | 
 |  | 
 | platform_arch_type = os.environ.get( | 
 |     "PLATFORM_ARCH_TYPE", "" | 
 | ) or BuiltIn().get_variable_value("${PLATFORM_ARCH_TYPE}", default="power") | 
 |  | 
 |  | 
 | def create_boot_table(file_path=None, os_host=""): | 
 |     r""" | 
 |     Read the boot table JSON file, convert it to an object and return it. | 
 |  | 
 |     Note that if the user is running without a global OS_HOST robot variable specified, this function will | 
 |     remove all of the "os_" start and end state requirements from the JSON data. | 
 |  | 
 |     Description of argument(s): | 
 |     file_path                       The path to the boot_table file.  If this value is not specified, it will | 
 |                                     be obtained from the "BOOT_TABLE_PATH" environment variable, if set. | 
 |                                     Otherwise, it will default to "data/boot_table.json".  If this value is a | 
 |                                     relative path, this function will use the code_base_dir_path as the base | 
 |                                     directory (see definition above). | 
 |     os_host                         The host name or IP address of the host associated with the machine being | 
 |                                     tested.  If the user is running without an OS_HOST (i.e. if this argument | 
 |                                     is blank), we remove os starting and ending state requirements from the | 
 |                                     boot entries. | 
 |     """ | 
 |     if file_path is None: | 
 |         if redfish_support_trans_state and platform_arch_type != "x86": | 
 |             file_path = os.environ.get( | 
 |                 "BOOT_TABLE_PATH", "data/boot_table_redfish.json" | 
 |             ) | 
 |         elif platform_arch_type == "x86": | 
 |             file_path = os.environ.get( | 
 |                 "BOOT_TABLE_PATH", "data/boot_table_x86.json" | 
 |             ) | 
 |         else: | 
 |             file_path = os.environ.get( | 
 |                 "BOOT_TABLE_PATH", "data/boot_table.json" | 
 |             ) | 
 |  | 
 |     if not file_path.startswith("/"): | 
 |         file_path = code_base_dir_path + file_path | 
 |  | 
 |     # Pre-process the file by removing blank lines and comment lines. | 
 |     temp = tempfile.NamedTemporaryFile() | 
 |     temp_file_path = temp.name | 
 |  | 
 |     cmd_buf = "egrep -v '^[ ]*$|^[ ]*#' " + file_path + " > " + temp_file_path | 
 |     gc.cmd_fnc_u(cmd_buf, quiet=1) | 
 |  | 
 |     boot_file = open(temp_file_path) | 
 |     boot_table = json.load(boot_file, object_hook=DotDict) | 
 |  | 
 |     # If the user is running without an OS_HOST, we remove os starting and ending state requirements from | 
 |     # the boot entries. | 
 |     if os_host == "": | 
 |         for boot in boot_table: | 
 |             state_keys = ["start", "end"] | 
 |             for state_key in state_keys: | 
 |                 for sub_state in list(boot_table[boot][state_key]): | 
 |                     if sub_state.startswith("os_"): | 
 |                         boot_table[boot][state_key].pop(sub_state, None) | 
 |  | 
 |     # For every boot_type we should have a corresponding mfg mode boot type. | 
 |     enhanced_boot_table = DotDict() | 
 |     for key, value in boot_table.items(): | 
 |         enhanced_boot_table[key] = value | 
 |         enhanced_boot_table[key + " (mfg)"] = value | 
 |  | 
 |     return enhanced_boot_table | 
 |  | 
 |  | 
 | def create_valid_boot_list(boot_table): | 
 |     r""" | 
 |     Return a list of all of the valid boot types (e.g. ['REST Power On', 'REST Power Off', ...]). | 
 |  | 
 |     Description of argument(s): | 
 |     boot_table                      A boot table such as is returned by the create_boot_table function. | 
 |     """ | 
 |  | 
 |     return list(boot_table.keys()) | 
 |  | 
 |  | 
 | def read_boot_lists(dir_path="data/boot_lists/"): | 
 |     r""" | 
 |     Read the contents of all the boot lists files found in the given boot lists directory and return | 
 |     dictionary of the lists. | 
 |  | 
 |     Boot lists are simply files containing a boot test name on each line.  These files are useful for | 
 |     categorizing and organizing boot tests.  For example, there may be a "Power_on" list, a "Power_off" list, | 
 |     etc. | 
 |  | 
 |     The names of the boot list files will be the keys to the top level dictionary.  Each dictionary entry is | 
 |     a list of all the boot tests found in the corresponding file. | 
 |  | 
 |     Here is an abbreviated look at the resulting boot_lists dictionary. | 
 |  | 
 |     boot_lists: | 
 |       boot_lists[All]: | 
 |         boot_lists[All][0]:                           REST Power On | 
 |         boot_lists[All][1]:                           REST Power Off | 
 |     ... | 
 |       boot_lists[Code_update]: | 
 |         boot_lists[Code_update][0]:                   BMC oob hpm | 
 |         boot_lists[Code_update][1]:                   BMC ib hpm | 
 |     ... | 
 |  | 
 |     Description of argument(s): | 
 |     dir_path                        The path to the directory containing the boot list files.  If this value | 
 |                                     is a relative path, this function will use the code_base_dir_path as the | 
 |                                     base directory (see definition above). | 
 |     """ | 
 |  | 
 |     if not dir_path.startswith("/"): | 
 |         # Dir path is relative. | 
 |         dir_path = code_base_dir_path + dir_path | 
 |  | 
 |     # Get a list of all file names in the directory. | 
 |     boot_file_names = os.listdir(dir_path) | 
 |  | 
 |     boot_lists = DotDict() | 
 |     for boot_category in boot_file_names: | 
 |         file_path = gm.which(dir_path + boot_category) | 
 |         boot_list = gm.file_to_list(file_path, newlines=0, comments=0, trim=1) | 
 |         boot_lists[boot_category] = boot_list | 
 |  | 
 |     return boot_lists | 
 |  | 
 |  | 
 | def valid_boot_list(boot_list, valid_boot_types): | 
 |     r""" | 
 |     Verify that each entry in boot_list is a supported boot test. | 
 |  | 
 |     Description of argument(s): | 
 |     boot_list                       An array (i.e. list) of boot test types (e.g. "REST Power On"). | 
 |     valid_boot_types                A list of valid boot types such as that returned by | 
 |                                     create_valid_boot_list. | 
 |     """ | 
 |  | 
 |     for boot_name in boot_list: | 
 |         boot_name = boot_name.strip(" ") | 
 |         error_message = gv.valid_value( | 
 |             boot_name, valid_values=valid_boot_types, var_name="boot_name" | 
 |         ) | 
 |         if error_message != "": | 
 |             BuiltIn().fail(gp.sprint_error(error_message)) | 
 |  | 
 |  | 
 | class boot_results: | 
 |     r""" | 
 |     This class defines a boot_results table. | 
 |     """ | 
 |  | 
 |     def __init__( | 
 |         self, boot_table, boot_pass=0, boot_fail=0, obj_name="boot_results" | 
 |     ): | 
 |         r""" | 
 |         Initialize the boot results object. | 
 |  | 
 |         Description of argument(s): | 
 |         boot_table                  Boot table object (see definition above).  The boot table contains all of | 
 |                                     the valid boot test types.  It can be created with the create_boot_table | 
 |                                     function. | 
 |         boot_pass                   An initial boot_pass value.  This program may be called as part of a | 
 |                                     larger test suite.  As such there may already have been some successful | 
 |                                     boot tests that we need to keep track of. | 
 |         boot_fail                   An initial boot_fail value.  This program may be called as part of a | 
 |                                     larger test suite.  As such there may already have been some unsuccessful | 
 |                                     boot tests that we need to keep track of. | 
 |         obj_name                    The name of this object. | 
 |         """ | 
 |  | 
 |         # Store the method parms as class data. | 
 |         self.__obj_name = obj_name | 
 |         self.__initial_boot_pass = boot_pass | 
 |         self.__initial_boot_fail = boot_fail | 
 |  | 
 |         # Create boot_results_fields for use in creating boot_results table. | 
 |         boot_results_fields = DotDict([("total", 0), ("pass", 0), ("fail", 0)]) | 
 |         # Create boot_results table. | 
 |         self.__boot_results = tally_sheet( | 
 |             "boot type", boot_results_fields, "boot_test_results" | 
 |         ) | 
 |         self.__boot_results.set_sum_fields(["total", "pass", "fail"]) | 
 |         self.__boot_results.set_calc_fields(["total=pass+fail"]) | 
 |         # Create one row in the result table for each kind of boot test in the boot_table (i.e. for all | 
 |         # supported boot tests). | 
 |         for boot_name in list(boot_table.keys()): | 
 |             self.__boot_results.add_row(boot_name) | 
 |  | 
 |     def add_row(self, *args, **kwargs): | 
 |         r""" | 
 |         Add row to tally_sheet class object. | 
 |  | 
 |         Description of argument(s): | 
 |         See add_row method in tally_sheet.py for a description of all arguments. | 
 |         """ | 
 |         self.__boot_results.add_row(*args, **kwargs) | 
 |  | 
 |     def return_total_pass_fail(self): | 
 |         r""" | 
 |         Return the total boot_pass and boot_fail values.  This information is comprised of the pass/fail | 
 |         values from the table plus the initial pass/fail values. | 
 |         """ | 
 |  | 
 |         totals_line = self.__boot_results.calc() | 
 |         return ( | 
 |             totals_line["pass"] + self.__initial_boot_pass, | 
 |             totals_line["fail"] + self.__initial_boot_fail, | 
 |         ) | 
 |  | 
 |     def update(self, boot_type, boot_status): | 
 |         r""" | 
 |         Update our boot_results_table.  This includes: | 
 |         - Updating the record for the given boot_type by incrementing the pass or fail field. | 
 |         - Calling the calc method to have the totals calculated. | 
 |  | 
 |         Description of argument(s): | 
 |         boot_type                   The type of boot test just done (e.g. "REST Power On"). | 
 |         boot_status                 The status of the boot just done.  This should be equal to either "pass" | 
 |                                     or "fail" (case-insensitive). | 
 |         """ | 
 |  | 
 |         self.__boot_results.inc_row_field(boot_type, boot_status.lower()) | 
 |         self.__boot_results.calc() | 
 |  | 
 |     def sprint_report(self, header_footer="\n"): | 
 |         r""" | 
 |         String-print the formatted boot_resuls_table and return them. | 
 |  | 
 |         Description of argument(s): | 
 |         header_footer               This indicates whether a header and footer are to be included in the | 
 |                                     report. | 
 |         """ | 
 |  | 
 |         buffer = "" | 
 |  | 
 |         buffer += gp.sprint(header_footer) | 
 |         buffer += self.__boot_results.sprint_report() | 
 |         buffer += gp.sprint(header_footer) | 
 |  | 
 |         return buffer | 
 |  | 
 |     def print_report(self, header_footer="\n", quiet=None): | 
 |         r""" | 
 |         Print the formatted boot_resuls_table to the console. | 
 |  | 
 |         Description of argument(s): | 
 |         See sprint_report for details. | 
 |         quiet                       Only print if this value is 0.  This function will search upward in the | 
 |                                     stack to get the default value. | 
 |         """ | 
 |  | 
 |         quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) | 
 |  | 
 |         gp.qprint(self.sprint_report(header_footer)) | 
 |  | 
 |     def sprint_obj(self): | 
 |         r""" | 
 |         sprint the fields of this object.  This would normally be for debug purposes only. | 
 |         """ | 
 |  | 
 |         buffer = "" | 
 |  | 
 |         buffer += "class name: " + self.__class__.__name__ + "\n" | 
 |         buffer += gp.sprint_var(self.__obj_name) | 
 |         buffer += self.__boot_results.sprint_obj() | 
 |         buffer += gp.sprint_var(self.__initial_boot_pass) | 
 |         buffer += gp.sprint_var(self.__initial_boot_fail) | 
 |  | 
 |         return buffer | 
 |  | 
 |     def print_obj(self): | 
 |         r""" | 
 |         Print the fields of this object to stdout.  This would normally be for debug purposes. | 
 |         """ | 
 |  | 
 |         gp.gp_print(self.sprint_obj()) | 
 |  | 
 |  | 
 | def create_boot_results_file_path(pgm_name, openbmc_nickname, master_pid): | 
 |     r""" | 
 |     Create a file path to be used to store a boot_results object. | 
 |  | 
 |     Description of argument(s): | 
 |     pgm_name                        The name of the program.  This will form part of the resulting file name. | 
 |     openbmc_nickname                The name of the system.  This could be a nickname, a hostname, an IP, | 
 |                                     etc.  This will form part of the resulting file name. | 
 |     master_pid                      The master process id which will form part of the file name. | 
 |     """ | 
 |  | 
 |     USER = os.environ.get("USER", "") | 
 |     dir_path = "/tmp/" + USER + "/" | 
 |     if not os.path.exists(dir_path): | 
 |         os.makedirs(dir_path) | 
 |  | 
 |     file_name_dict = vf.create_var_dict(pgm_name, openbmc_nickname, master_pid) | 
 |     return vf.create_file_path( | 
 |         file_name_dict, dir_path=dir_path, file_suffix=":boot_results" | 
 |     ) | 
 |  | 
 |  | 
 | def cleanup_boot_results_file(): | 
 |     r""" | 
 |     Delete all boot results files whose corresponding pids are no longer active. | 
 |     """ | 
 |  | 
 |     # Use create_boot_results_file_path to create a globex to find all of the existing boot results files. | 
 |     globex = create_boot_results_file_path("*", "*", "*") | 
 |     file_list = sorted(glob.glob(globex)) | 
 |     for file_path in file_list: | 
 |         # Use parse_file_path to extract info from the file path. | 
 |         file_dict = vf.parse_file_path(file_path) | 
 |         if gm.pid_active(file_dict["master_pid"]): | 
 |             gp.qprint_timen("Preserving " + file_path + ".") | 
 |         else: | 
 |             gc.cmd_fnc("rm -f " + file_path) | 
 |  | 
 |  | 
 | def update_boot_history(boot_history, boot_start_message, max_boot_history=10): | 
 |     r""" | 
 |     Update the boot_history list by appending the boot_start_message and by removing all but the last n | 
 |     entries. | 
 |  | 
 |     Description of argument(s): | 
 |     boot_history                    A list of boot start messages. | 
 |     boot_start_message              This is typically a time-stamped line of text announcing the start of a | 
 |                                     boot test. | 
 |     max_boot_history                The max number of entries to be kept in the boot_history list.  The | 
 |                                     oldest entries are deleted to achieve this list size. | 
 |     """ | 
 |  | 
 |     boot_history.append(boot_start_message) | 
 |  | 
 |     # Trim list to max number of entries. | 
 |     del boot_history[: max(0, len(boot_history) - max_boot_history)] | 
 |  | 
 |  | 
 | def print_boot_history(boot_history, quiet=None): | 
 |     r""" | 
 |     Print the last ten boots done with their time stamps. | 
 |  | 
 |     Description of argument(s): | 
 |     quiet                           Only print if this value is 0.  This function will search upward in the | 
 |                                     stack to get the default value. | 
 |     """ | 
 |  | 
 |     quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) | 
 |  | 
 |     # indent 0, 90 chars wide, linefeed, char is "=" | 
 |     gp.qprint_dashes(0, 90) | 
 |     gp.qprintn("Last 10 boots:\n") | 
 |  | 
 |     for boot_entry in boot_history: | 
 |         gp.qprint(boot_entry) | 
 |     gp.qprint_dashes(0, 90) |