#!/usr/bin/env python r""" This module has functions to support various data structures such as the boot_table, valid_boot_list and boot_results_table. """ import os import tempfile import json import glob from tally_sheet import * from robot.libraries.BuiltIn import BuiltIn try: from robot.utils import DotDict except ImportError: import collections import gen_print as gp import gen_valid as gv import gen_misc as gm import gen_cmd as gc 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)) 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: file_path = os.environ.get('BOOT_TABLE_PATH', 'data/boot_table_redfish.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)