1e7e9171eSGeorge Keishing#!/usr/bin/env python3 2ac29d063SMichael Walsh 3ac29d063SMichael Walshr""" 4410b1787SMichael WalshThis module has functions to support various data structures such as the boot_table, valid_boot_list and 5410b1787SMichael Walshboot_results_table. 6ac29d063SMichael Walsh""" 7ac29d063SMichael Walsh 8*20f38712SPatrick Williamsimport glob 9*20f38712SPatrick Williamsimport json 10ac29d063SMichael Walshimport os 11ac29d063SMichael Walshimport tempfile 125731818dSPatrick Williams 13e635ddc0SGeorge Keishingfrom robot.libraries.BuiltIn import BuiltIn 14*20f38712SPatrick Williamsfrom tally_sheet import * 15*20f38712SPatrick Williams 16ac29d063SMichael Walshtry: 17ac29d063SMichael Walsh from robot.utils import DotDict 18ac29d063SMichael Walshexcept ImportError: 19ac29d063SMichael Walsh import collections 20ac29d063SMichael Walsh 21*20f38712SPatrick Williamsimport gen_cmd as gc 22*20f38712SPatrick Williamsimport gen_misc as gm 23ac29d063SMichael Walshimport gen_print as gp 24ac29d063SMichael Walshimport gen_valid as gv 25b6e3aacdSMichael Walshimport var_funcs as vf 26ac29d063SMichael Walsh 27410b1787SMichael Walsh# The code base directory will be one level up from the directory containing this module. 28ac29d063SMichael Walshcode_base_dir_path = os.path.dirname(os.path.dirname(__file__)) + os.sep 29ac29d063SMichael Walsh 30*20f38712SPatrick Williamsredfish_support_trans_state = int( 31*20f38712SPatrick Williams os.environ.get("REDFISH_SUPPORT_TRANS_STATE", 0) 32*20f38712SPatrick Williams) or int( 33*20f38712SPatrick Williams BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0) 34*20f38712SPatrick Williams) 35da40c1d2SMichael Shepos 36*20f38712SPatrick Williamsplatform_arch_type = os.environ.get( 37*20f38712SPatrick Williams "PLATFORM_ARCH_TYPE", "" 38*20f38712SPatrick Williams) or BuiltIn().get_variable_value("${PLATFORM_ARCH_TYPE}", default="power") 391e2fbee9SGeorge Keishing 40ac29d063SMichael Walsh 41*20f38712SPatrick Williamsdef create_boot_table(file_path=None, os_host=""): 42ac29d063SMichael Walsh r""" 43ac29d063SMichael Walsh Read the boot table JSON file, convert it to an object and return it. 44ac29d063SMichael Walsh 45410b1787SMichael Walsh Note that if the user is running without a global OS_HOST robot variable specified, this function will 46410b1787SMichael Walsh remove all of the "os_" start and end state requirements from the JSON data. 47ac29d063SMichael Walsh 486c4520c6SMichael Walsh Description of argument(s): 49410b1787SMichael Walsh file_path The path to the boot_table file. If this value is not specified, it will 50410b1787SMichael Walsh be obtained from the "BOOT_TABLE_PATH" environment variable, if set. 51410b1787SMichael Walsh Otherwise, it will default to "data/boot_table.json". If this value is a 52410b1787SMichael Walsh relative path, this function will use the code_base_dir_path as the base 53410b1787SMichael Walsh directory (see definition above). 54410b1787SMichael Walsh os_host The host name or IP address of the host associated with the machine being 55410b1787SMichael Walsh tested. If the user is running without an OS_HOST (i.e. if this argument 56410b1787SMichael Walsh is blank), we remove os starting and ending state requirements from the 57410b1787SMichael Walsh boot entries. 58ac29d063SMichael Walsh """ 59ac29d063SMichael Walsh if file_path is None: 60b51d1505SGeorge Keishing if redfish_support_trans_state and platform_arch_type != "x86": 61*20f38712SPatrick Williams file_path = os.environ.get( 62*20f38712SPatrick Williams "BOOT_TABLE_PATH", "data/boot_table_redfish.json" 63*20f38712SPatrick Williams ) 641e2fbee9SGeorge Keishing elif platform_arch_type == "x86": 65*20f38712SPatrick Williams file_path = os.environ.get( 66*20f38712SPatrick Williams "BOOT_TABLE_PATH", "data/boot_table_x86.json" 67*20f38712SPatrick Williams ) 68da40c1d2SMichael Shepos else: 69*20f38712SPatrick Williams file_path = os.environ.get( 70*20f38712SPatrick Williams "BOOT_TABLE_PATH", "data/boot_table.json" 71*20f38712SPatrick Williams ) 72ac29d063SMichael Walsh 73ac29d063SMichael Walsh if not file_path.startswith("/"): 74ac29d063SMichael Walsh file_path = code_base_dir_path + file_path 75ac29d063SMichael Walsh 76ac29d063SMichael Walsh # Pre-process the file by removing blank lines and comment lines. 77ac29d063SMichael Walsh temp = tempfile.NamedTemporaryFile() 78ac29d063SMichael Walsh temp_file_path = temp.name 79ac29d063SMichael Walsh 80ac29d063SMichael Walsh cmd_buf = "egrep -v '^[ ]*$|^[ ]*#' " + file_path + " > " + temp_file_path 81ac29d063SMichael Walsh gc.cmd_fnc_u(cmd_buf, quiet=1) 82ac29d063SMichael Walsh 83ac29d063SMichael Walsh boot_file = open(temp_file_path) 84ac29d063SMichael Walsh boot_table = json.load(boot_file, object_hook=DotDict) 85ac29d063SMichael Walsh 86410b1787SMichael Walsh # If the user is running without an OS_HOST, we remove os starting and ending state requirements from 87410b1787SMichael Walsh # the boot entries. 88ac29d063SMichael Walsh if os_host == "": 89ac29d063SMichael Walsh for boot in boot_table: 90*20f38712SPatrick Williams state_keys = ["start", "end"] 91ac29d063SMichael Walsh for state_key in state_keys: 9237f833d0SMichael Walsh for sub_state in list(boot_table[boot][state_key]): 93ac29d063SMichael Walsh if sub_state.startswith("os_"): 94ac29d063SMichael Walsh boot_table[boot][state_key].pop(sub_state, None) 95ac29d063SMichael Walsh 9607a01ef6SMichael Walsh # For every boot_type we should have a corresponding mfg mode boot type. 9707a01ef6SMichael Walsh enhanced_boot_table = DotDict() 9836efbc04SGeorge Keishing for key, value in boot_table.items(): 9907a01ef6SMichael Walsh enhanced_boot_table[key] = value 10007a01ef6SMichael Walsh enhanced_boot_table[key + " (mfg)"] = value 10107a01ef6SMichael Walsh 10207a01ef6SMichael Walsh return enhanced_boot_table 103ac29d063SMichael Walsh 104ac29d063SMichael Walsh 105ac29d063SMichael Walshdef create_valid_boot_list(boot_table): 106ac29d063SMichael Walsh r""" 107410b1787SMichael Walsh Return a list of all of the valid boot types (e.g. ['REST Power On', 'REST Power Off', ...]). 108ac29d063SMichael Walsh 1096c4520c6SMichael Walsh Description of argument(s): 110410b1787SMichael Walsh boot_table A boot table such as is returned by the create_boot_table function. 111ac29d063SMichael Walsh """ 112ac29d063SMichael Walsh 113ac29d063SMichael Walsh return list(boot_table.keys()) 114ac29d063SMichael Walsh 115ac29d063SMichael Walsh 116ac29d063SMichael Walshdef read_boot_lists(dir_path="data/boot_lists/"): 117ac29d063SMichael Walsh r""" 118410b1787SMichael Walsh Read the contents of all the boot lists files found in the given boot lists directory and return 119410b1787SMichael Walsh dictionary of the lists. 120ac29d063SMichael Walsh 121410b1787SMichael Walsh Boot lists are simply files containing a boot test name on each line. These files are useful for 122410b1787SMichael Walsh categorizing and organizing boot tests. For example, there may be a "Power_on" list, a "Power_off" list, 123410b1787SMichael Walsh etc. 124ac29d063SMichael Walsh 125410b1787SMichael Walsh The names of the boot list files will be the keys to the top level dictionary. Each dictionary entry is 126410b1787SMichael Walsh a list of all the boot tests found in the corresponding file. 127ac29d063SMichael Walsh 128ac29d063SMichael Walsh Here is an abbreviated look at the resulting boot_lists dictionary. 129ac29d063SMichael Walsh 130ac29d063SMichael Walsh boot_lists: 131ac29d063SMichael Walsh boot_lists[All]: 1326c4520c6SMichael Walsh boot_lists[All][0]: REST Power On 1336c4520c6SMichael Walsh boot_lists[All][1]: REST Power Off 134ac29d063SMichael Walsh ... 135ac29d063SMichael Walsh boot_lists[Code_update]: 136ac29d063SMichael Walsh boot_lists[Code_update][0]: BMC oob hpm 137ac29d063SMichael Walsh boot_lists[Code_update][1]: BMC ib hpm 138ac29d063SMichael Walsh ... 139ac29d063SMichael Walsh 1406c4520c6SMichael Walsh Description of argument(s): 141410b1787SMichael Walsh dir_path The path to the directory containing the boot list files. If this value 142410b1787SMichael Walsh is a relative path, this function will use the code_base_dir_path as the 143410b1787SMichael Walsh base directory (see definition above). 144ac29d063SMichael Walsh """ 145ac29d063SMichael Walsh 146ac29d063SMichael Walsh if not dir_path.startswith("/"): 147ac29d063SMichael Walsh # Dir path is relative. 148ac29d063SMichael Walsh dir_path = code_base_dir_path + dir_path 149ac29d063SMichael Walsh 150ac29d063SMichael Walsh # Get a list of all file names in the directory. 151ac29d063SMichael Walsh boot_file_names = os.listdir(dir_path) 152ac29d063SMichael Walsh 153ac29d063SMichael Walsh boot_lists = DotDict() 154ac29d063SMichael Walsh for boot_category in boot_file_names: 155ac29d063SMichael Walsh file_path = gm.which(dir_path + boot_category) 156ac29d063SMichael Walsh boot_list = gm.file_to_list(file_path, newlines=0, comments=0, trim=1) 157ac29d063SMichael Walsh boot_lists[boot_category] = boot_list 158ac29d063SMichael Walsh 159ac29d063SMichael Walsh return boot_lists 160ac29d063SMichael Walsh 161ac29d063SMichael Walsh 162*20f38712SPatrick Williamsdef valid_boot_list(boot_list, valid_boot_types): 163ac29d063SMichael Walsh r""" 164ac29d063SMichael Walsh Verify that each entry in boot_list is a supported boot test. 165ac29d063SMichael Walsh 1666c4520c6SMichael Walsh Description of argument(s): 167410b1787SMichael Walsh boot_list An array (i.e. list) of boot test types (e.g. "REST Power On"). 168410b1787SMichael Walsh valid_boot_types A list of valid boot types such as that returned by 169410b1787SMichael Walsh create_valid_boot_list. 170ac29d063SMichael Walsh """ 171ac29d063SMichael Walsh 172ac29d063SMichael Walsh for boot_name in boot_list: 173ac29d063SMichael Walsh boot_name = boot_name.strip(" ") 174*20f38712SPatrick Williams error_message = gv.valid_value( 175*20f38712SPatrick Williams boot_name, valid_values=valid_boot_types, var_name="boot_name" 176*20f38712SPatrick Williams ) 177ac29d063SMichael Walsh if error_message != "": 178ac29d063SMichael Walsh BuiltIn().fail(gp.sprint_error(error_message)) 179ac29d063SMichael Walsh 180ac29d063SMichael Walsh 181ac29d063SMichael Walshclass boot_results: 182ac29d063SMichael Walsh r""" 183ac29d063SMichael Walsh This class defines a boot_results table. 184ac29d063SMichael Walsh """ 185ac29d063SMichael Walsh 186*20f38712SPatrick Williams def __init__( 187*20f38712SPatrick Williams self, boot_table, boot_pass=0, boot_fail=0, obj_name="boot_results" 188*20f38712SPatrick Williams ): 189ac29d063SMichael Walsh r""" 190ac29d063SMichael Walsh Initialize the boot results object. 191ac29d063SMichael Walsh 1926c4520c6SMichael Walsh Description of argument(s): 193410b1787SMichael Walsh boot_table Boot table object (see definition above). The boot table contains all of 194410b1787SMichael Walsh the valid boot test types. It can be created with the create_boot_table 195410b1787SMichael Walsh function. 196410b1787SMichael Walsh boot_pass An initial boot_pass value. This program may be called as part of a 197410b1787SMichael Walsh larger test suite. As such there may already have been some successful 198410b1787SMichael Walsh boot tests that we need to keep track of. 199410b1787SMichael Walsh boot_fail An initial boot_fail value. This program may be called as part of a 200410b1787SMichael Walsh larger test suite. As such there may already have been some unsuccessful 201410b1787SMichael Walsh boot tests that we need to keep track of. 202ac29d063SMichael Walsh obj_name The name of this object. 203ac29d063SMichael Walsh """ 204ac29d063SMichael Walsh 205ac29d063SMichael Walsh # Store the method parms as class data. 206ac29d063SMichael Walsh self.__obj_name = obj_name 207ac29d063SMichael Walsh self.__initial_boot_pass = boot_pass 208ac29d063SMichael Walsh self.__initial_boot_fail = boot_fail 209ac29d063SMichael Walsh 210ac29d063SMichael Walsh # Create boot_results_fields for use in creating boot_results table. 211*20f38712SPatrick Williams boot_results_fields = DotDict([("total", 0), ("pass", 0), ("fail", 0)]) 212ac29d063SMichael Walsh # Create boot_results table. 213*20f38712SPatrick Williams self.__boot_results = tally_sheet( 214*20f38712SPatrick Williams "boot type", boot_results_fields, "boot_test_results" 215*20f38712SPatrick Williams ) 216*20f38712SPatrick Williams self.__boot_results.set_sum_fields(["total", "pass", "fail"]) 217*20f38712SPatrick Williams self.__boot_results.set_calc_fields(["total=pass+fail"]) 218410b1787SMichael Walsh # Create one row in the result table for each kind of boot test in the boot_table (i.e. for all 219410b1787SMichael Walsh # supported boot tests). 220ac29d063SMichael Walsh for boot_name in list(boot_table.keys()): 221ac29d063SMichael Walsh self.__boot_results.add_row(boot_name) 222ac29d063SMichael Walsh 2236c4520c6SMichael Walsh def add_row(self, *args, **kwargs): 2246c4520c6SMichael Walsh r""" 2256c4520c6SMichael Walsh Add row to tally_sheet class object. 2266c4520c6SMichael Walsh 2276c4520c6SMichael Walsh Description of argument(s): 228410b1787SMichael Walsh See add_row method in tally_sheet.py for a description of all arguments. 2296c4520c6SMichael Walsh """ 2306c4520c6SMichael Walsh self.__boot_results.add_row(*args, **kwargs) 2316c4520c6SMichael Walsh 232ac29d063SMichael Walsh def return_total_pass_fail(self): 233ac29d063SMichael Walsh r""" 234410b1787SMichael Walsh Return the total boot_pass and boot_fail values. This information is comprised of the pass/fail 235410b1787SMichael Walsh values from the table plus the initial pass/fail values. 236ac29d063SMichael Walsh """ 237ac29d063SMichael Walsh 238ac29d063SMichael Walsh totals_line = self.__boot_results.calc() 239*20f38712SPatrick Williams return ( 240*20f38712SPatrick Williams totals_line["pass"] + self.__initial_boot_pass, 241*20f38712SPatrick Williams totals_line["fail"] + self.__initial_boot_fail, 242*20f38712SPatrick Williams ) 243ac29d063SMichael Walsh 244*20f38712SPatrick Williams def update(self, boot_type, boot_status): 245ac29d063SMichael Walsh r""" 246ac29d063SMichael Walsh Update our boot_results_table. This includes: 247410b1787SMichael Walsh - Updating the record for the given boot_type by incrementing the pass or fail field. 248ac29d063SMichael Walsh - Calling the calc method to have the totals calculated. 249ac29d063SMichael Walsh 2506c4520c6SMichael Walsh Description of argument(s): 251410b1787SMichael Walsh boot_type The type of boot test just done (e.g. "REST Power On"). 252410b1787SMichael Walsh boot_status The status of the boot just done. This should be equal to either "pass" 253410b1787SMichael Walsh or "fail" (case-insensitive). 254ac29d063SMichael Walsh """ 255ac29d063SMichael Walsh 256ac29d063SMichael Walsh self.__boot_results.inc_row_field(boot_type, boot_status.lower()) 2578f1ef9eaSMichael Walsh self.__boot_results.calc() 258ac29d063SMichael Walsh 259*20f38712SPatrick Williams def sprint_report(self, header_footer="\n"): 260ac29d063SMichael Walsh r""" 261ac29d063SMichael Walsh String-print the formatted boot_resuls_table and return them. 262ac29d063SMichael Walsh 2636c4520c6SMichael Walsh Description of argument(s): 264410b1787SMichael Walsh header_footer This indicates whether a header and footer are to be included in the 265410b1787SMichael Walsh report. 266ac29d063SMichael Walsh """ 267ac29d063SMichael Walsh 268ac29d063SMichael Walsh buffer = "" 269ac29d063SMichael Walsh 270ac29d063SMichael Walsh buffer += gp.sprint(header_footer) 271ac29d063SMichael Walsh buffer += self.__boot_results.sprint_report() 272ac29d063SMichael Walsh buffer += gp.sprint(header_footer) 273ac29d063SMichael Walsh 274ac29d063SMichael Walsh return buffer 275ac29d063SMichael Walsh 276*20f38712SPatrick Williams def print_report(self, header_footer="\n", quiet=None): 277ac29d063SMichael Walsh r""" 278ac29d063SMichael Walsh Print the formatted boot_resuls_table to the console. 279ac29d063SMichael Walsh 2806c4520c6SMichael Walsh Description of argument(s): 281ac29d063SMichael Walsh See sprint_report for details. 282410b1787SMichael Walsh quiet Only print if this value is 0. This function will search upward in the 283410b1787SMichael Walsh stack to get the default value. 284ac29d063SMichael Walsh """ 285ac29d063SMichael Walsh 286*20f38712SPatrick Williams quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) 2876c4520c6SMichael Walsh 288c108e429SMichael Walsh gp.qprint(self.sprint_report(header_footer)) 289ac29d063SMichael Walsh 290ac29d063SMichael Walsh def sprint_obj(self): 291ac29d063SMichael Walsh r""" 292410b1787SMichael Walsh sprint the fields of this object. This would normally be for debug purposes only. 293ac29d063SMichael Walsh """ 294ac29d063SMichael Walsh 295ac29d063SMichael Walsh buffer = "" 296ac29d063SMichael Walsh 297ac29d063SMichael Walsh buffer += "class name: " + self.__class__.__name__ + "\n" 298ac29d063SMichael Walsh buffer += gp.sprint_var(self.__obj_name) 299ac29d063SMichael Walsh buffer += self.__boot_results.sprint_obj() 300ac29d063SMichael Walsh buffer += gp.sprint_var(self.__initial_boot_pass) 301ac29d063SMichael Walsh buffer += gp.sprint_var(self.__initial_boot_fail) 302ac29d063SMichael Walsh 303ac29d063SMichael Walsh return buffer 304ac29d063SMichael Walsh 305ac29d063SMichael Walsh def print_obj(self): 306ac29d063SMichael Walsh r""" 307410b1787SMichael Walsh Print the fields of this object to stdout. This would normally be for debug purposes. 308ac29d063SMichael Walsh """ 309ac29d063SMichael Walsh 310c108e429SMichael Walsh gp.gp_print(self.sprint_obj()) 311ac29d063SMichael Walsh 312b6e3aacdSMichael Walsh 313*20f38712SPatrick Williamsdef create_boot_results_file_path(pgm_name, openbmc_nickname, master_pid): 314b6e3aacdSMichael Walsh r""" 315b6e3aacdSMichael Walsh Create a file path to be used to store a boot_results object. 316b6e3aacdSMichael Walsh 317b6e3aacdSMichael Walsh Description of argument(s): 318410b1787SMichael Walsh pgm_name The name of the program. This will form part of the resulting file name. 319410b1787SMichael Walsh openbmc_nickname The name of the system. This could be a nickname, a hostname, an IP, 320410b1787SMichael Walsh etc. This will form part of the resulting file name. 321410b1787SMichael Walsh master_pid The master process id which will form part of the file name. 322b6e3aacdSMichael Walsh """ 323b6e3aacdSMichael Walsh 3248d7b7383SMichael Walsh USER = os.environ.get("USER", "") 3258d7b7383SMichael Walsh dir_path = "/tmp/" + USER + "/" 3268d7b7383SMichael Walsh if not os.path.exists(dir_path): 3278d7b7383SMichael Walsh os.makedirs(dir_path) 3288d7b7383SMichael Walsh 329b6e3aacdSMichael Walsh file_name_dict = vf.create_var_dict(pgm_name, openbmc_nickname, master_pid) 330*20f38712SPatrick Williams return vf.create_file_path( 331*20f38712SPatrick Williams file_name_dict, dir_path=dir_path, file_suffix=":boot_results" 332*20f38712SPatrick Williams ) 333b6e3aacdSMichael Walsh 334b6e3aacdSMichael Walsh 335b6e3aacdSMichael Walshdef cleanup_boot_results_file(): 336b6e3aacdSMichael Walsh r""" 337410b1787SMichael Walsh Delete all boot results files whose corresponding pids are no longer active. 338b6e3aacdSMichael Walsh """ 339b6e3aacdSMichael Walsh 340410b1787SMichael Walsh # Use create_boot_results_file_path to create a globex to find all of the existing boot results files. 341b6e3aacdSMichael Walsh globex = create_boot_results_file_path("*", "*", "*") 342b6e3aacdSMichael Walsh file_list = sorted(glob.glob(globex)) 343b6e3aacdSMichael Walsh for file_path in file_list: 344b6e3aacdSMichael Walsh # Use parse_file_path to extract info from the file path. 345b6e3aacdSMichael Walsh file_dict = vf.parse_file_path(file_path) 346*20f38712SPatrick Williams if gm.pid_active(file_dict["master_pid"]): 347b6e3aacdSMichael Walsh gp.qprint_timen("Preserving " + file_path + ".") 348b6e3aacdSMichael Walsh else: 349b6e3aacdSMichael Walsh gc.cmd_fnc("rm -f " + file_path) 3506c4520c6SMichael Walsh 3516c4520c6SMichael Walsh 3526c4520c6SMichael Walshdef update_boot_history(boot_history, boot_start_message, max_boot_history=10): 3536c4520c6SMichael Walsh r""" 354410b1787SMichael Walsh Update the boot_history list by appending the boot_start_message and by removing all but the last n 355410b1787SMichael Walsh entries. 3566c4520c6SMichael Walsh 3576c4520c6SMichael Walsh Description of argument(s): 3586c4520c6SMichael Walsh boot_history A list of boot start messages. 359410b1787SMichael Walsh boot_start_message This is typically a time-stamped line of text announcing the start of a 360410b1787SMichael Walsh boot test. 361410b1787SMichael Walsh max_boot_history The max number of entries to be kept in the boot_history list. The 362410b1787SMichael Walsh oldest entries are deleted to achieve this list size. 3636c4520c6SMichael Walsh """ 3646c4520c6SMichael Walsh 3656c4520c6SMichael Walsh boot_history.append(boot_start_message) 3666c4520c6SMichael Walsh 3676c4520c6SMichael Walsh # Trim list to max number of entries. 3686c4520c6SMichael Walsh del boot_history[: max(0, len(boot_history) - max_boot_history)] 3696c4520c6SMichael Walsh 3706c4520c6SMichael Walsh 3716c4520c6SMichael Walshdef print_boot_history(boot_history, quiet=None): 3726c4520c6SMichael Walsh r""" 3736c4520c6SMichael Walsh Print the last ten boots done with their time stamps. 3746c4520c6SMichael Walsh 3756c4520c6SMichael Walsh Description of argument(s): 376410b1787SMichael Walsh quiet Only print if this value is 0. This function will search upward in the 377410b1787SMichael Walsh stack to get the default value. 3786c4520c6SMichael Walsh """ 3796c4520c6SMichael Walsh 380*20f38712SPatrick Williams quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) 3816c4520c6SMichael Walsh 3826c4520c6SMichael Walsh # indent 0, 90 chars wide, linefeed, char is "=" 3836c4520c6SMichael Walsh gp.qprint_dashes(0, 90) 3846c4520c6SMichael Walsh gp.qprintn("Last 10 boots:\n") 3856c4520c6SMichael Walsh 3866c4520c6SMichael Walsh for boot_entry in boot_history: 3876c4520c6SMichael Walsh gp.qprint(boot_entry) 3886c4520c6SMichael Walsh gp.qprint_dashes(0, 90) 389