1#!/usr/bin/env python3 2 3r""" 4This module has functions to support various data structures such as the boot_table, valid_boot_list and 5boot_results_table. 6""" 7 8import glob 9import json 10import os 11import tempfile 12 13from robot.libraries.BuiltIn import BuiltIn 14from tally_sheet import * 15 16try: 17 from robot.utils import DotDict 18except ImportError: 19 import collections 20 21import gen_cmd as gc 22import gen_misc as gm 23import gen_print as gp 24import gen_valid as gv 25import var_funcs as vf 26 27# The code base directory will be one level up from the directory containing this module. 28code_base_dir_path = os.path.dirname(os.path.dirname(__file__)) + os.sep 29 30redfish_support_trans_state = int( 31 os.environ.get("REDFISH_SUPPORT_TRANS_STATE", 0) 32) or int( 33 BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0) 34) 35 36platform_arch_type = os.environ.get( 37 "PLATFORM_ARCH_TYPE", "" 38) or BuiltIn().get_variable_value("${PLATFORM_ARCH_TYPE}", default="power") 39 40 41def create_boot_table(file_path=None, os_host=""): 42 r""" 43 Read the boot table JSON file, convert it to an object and return it. 44 45 Note that if the user is running without a global OS_HOST robot variable specified, this function will 46 remove all of the "os_" start and end state requirements from the JSON data. 47 48 Description of argument(s): 49 file_path The path to the boot_table file. If this value is not specified, it will 50 be obtained from the "BOOT_TABLE_PATH" environment variable, if set. 51 Otherwise, it will default to "data/boot_table.json". If this value is a 52 relative path, this function will use the code_base_dir_path as the base 53 directory (see definition above). 54 os_host The host name or IP address of the host associated with the machine being 55 tested. If the user is running without an OS_HOST (i.e. if this argument 56 is blank), we remove os starting and ending state requirements from the 57 boot entries. 58 """ 59 if file_path is None: 60 if redfish_support_trans_state and platform_arch_type != "x86": 61 file_path = os.environ.get( 62 "BOOT_TABLE_PATH", "data/boot_table_redfish.json" 63 ) 64 elif platform_arch_type == "x86": 65 file_path = os.environ.get( 66 "BOOT_TABLE_PATH", "data/boot_table_x86.json" 67 ) 68 else: 69 file_path = os.environ.get( 70 "BOOT_TABLE_PATH", "data/boot_table.json" 71 ) 72 73 if not file_path.startswith("/"): 74 file_path = code_base_dir_path + file_path 75 76 # Pre-process the file by removing blank lines and comment lines. 77 temp = tempfile.NamedTemporaryFile() 78 temp_file_path = temp.name 79 80 cmd_buf = "egrep -v '^[ ]*$|^[ ]*#' " + file_path + " > " + temp_file_path 81 gc.cmd_fnc_u(cmd_buf, quiet=1) 82 83 boot_file = open(temp_file_path) 84 boot_table = json.load(boot_file, object_hook=DotDict) 85 86 # If the user is running without an OS_HOST, we remove os starting and ending state requirements from 87 # the boot entries. 88 if os_host == "": 89 for boot in boot_table: 90 state_keys = ["start", "end"] 91 for state_key in state_keys: 92 for sub_state in list(boot_table[boot][state_key]): 93 if sub_state.startswith("os_"): 94 boot_table[boot][state_key].pop(sub_state, None) 95 96 # For every boot_type we should have a corresponding mfg mode boot type. 97 enhanced_boot_table = DotDict() 98 for key, value in boot_table.items(): 99 enhanced_boot_table[key] = value 100 enhanced_boot_table[key + " (mfg)"] = value 101 102 return enhanced_boot_table 103 104 105def create_valid_boot_list(boot_table): 106 r""" 107 Return a list of all of the valid boot types (e.g. ['REST Power On', 'REST Power Off', ...]). 108 109 Description of argument(s): 110 boot_table A boot table such as is returned by the create_boot_table function. 111 """ 112 113 return list(boot_table.keys()) 114 115 116def read_boot_lists(dir_path="data/boot_lists/"): 117 r""" 118 Read the contents of all the boot lists files found in the given boot lists directory and return 119 dictionary of the lists. 120 121 Boot lists are simply files containing a boot test name on each line. These files are useful for 122 categorizing and organizing boot tests. For example, there may be a "Power_on" list, a "Power_off" list, 123 etc. 124 125 The names of the boot list files will be the keys to the top level dictionary. Each dictionary entry is 126 a list of all the boot tests found in the corresponding file. 127 128 Here is an abbreviated look at the resulting boot_lists dictionary. 129 130 boot_lists: 131 boot_lists[All]: 132 boot_lists[All][0]: REST Power On 133 boot_lists[All][1]: REST Power Off 134 ... 135 boot_lists[Code_update]: 136 boot_lists[Code_update][0]: BMC oob hpm 137 boot_lists[Code_update][1]: BMC ib hpm 138 ... 139 140 Description of argument(s): 141 dir_path The path to the directory containing the boot list files. If this value 142 is a relative path, this function will use the code_base_dir_path as the 143 base directory (see definition above). 144 """ 145 146 if not dir_path.startswith("/"): 147 # Dir path is relative. 148 dir_path = code_base_dir_path + dir_path 149 150 # Get a list of all file names in the directory. 151 boot_file_names = os.listdir(dir_path) 152 153 boot_lists = DotDict() 154 for boot_category in boot_file_names: 155 file_path = gm.which(dir_path + boot_category) 156 boot_list = gm.file_to_list(file_path, newlines=0, comments=0, trim=1) 157 boot_lists[boot_category] = boot_list 158 159 return boot_lists 160 161 162def valid_boot_list(boot_list, valid_boot_types): 163 r""" 164 Verify that each entry in boot_list is a supported boot test. 165 166 Description of argument(s): 167 boot_list An array (i.e. list) of boot test types (e.g. "REST Power On"). 168 valid_boot_types A list of valid boot types such as that returned by 169 create_valid_boot_list. 170 """ 171 172 for boot_name in boot_list: 173 boot_name = boot_name.strip(" ") 174 error_message = gv.valid_value( 175 boot_name, valid_values=valid_boot_types, var_name="boot_name" 176 ) 177 if error_message != "": 178 BuiltIn().fail(gp.sprint_error(error_message)) 179 180 181class boot_results: 182 r""" 183 This class defines a boot_results table. 184 """ 185 186 def __init__( 187 self, boot_table, boot_pass=0, boot_fail=0, obj_name="boot_results" 188 ): 189 r""" 190 Initialize the boot results object. 191 192 Description of argument(s): 193 boot_table Boot table object (see definition above). The boot table contains all of 194 the valid boot test types. It can be created with the create_boot_table 195 function. 196 boot_pass An initial boot_pass value. This program may be called as part of a 197 larger test suite. As such there may already have been some successful 198 boot tests that we need to keep track of. 199 boot_fail An initial boot_fail value. This program may be called as part of a 200 larger test suite. As such there may already have been some unsuccessful 201 boot tests that we need to keep track of. 202 obj_name The name of this object. 203 """ 204 205 # Store the method parms as class data. 206 self.__obj_name = obj_name 207 self.__initial_boot_pass = boot_pass 208 self.__initial_boot_fail = boot_fail 209 210 # Create boot_results_fields for use in creating boot_results table. 211 boot_results_fields = DotDict([("total", 0), ("pass", 0), ("fail", 0)]) 212 # Create boot_results table. 213 self.__boot_results = tally_sheet( 214 "boot type", boot_results_fields, "boot_test_results" 215 ) 216 self.__boot_results.set_sum_fields(["total", "pass", "fail"]) 217 self.__boot_results.set_calc_fields(["total=pass+fail"]) 218 # Create one row in the result table for each kind of boot test in the boot_table (i.e. for all 219 # supported boot tests). 220 for boot_name in list(boot_table.keys()): 221 self.__boot_results.add_row(boot_name) 222 223 def add_row(self, *args, **kwargs): 224 r""" 225 Add row to tally_sheet class object. 226 227 Description of argument(s): 228 See add_row method in tally_sheet.py for a description of all arguments. 229 """ 230 self.__boot_results.add_row(*args, **kwargs) 231 232 def return_total_pass_fail(self): 233 r""" 234 Return the total boot_pass and boot_fail values. This information is comprised of the pass/fail 235 values from the table plus the initial pass/fail values. 236 """ 237 238 totals_line = self.__boot_results.calc() 239 return ( 240 totals_line["pass"] + self.__initial_boot_pass, 241 totals_line["fail"] + self.__initial_boot_fail, 242 ) 243 244 def update(self, boot_type, boot_status): 245 r""" 246 Update our boot_results_table. This includes: 247 - Updating the record for the given boot_type by incrementing the pass or fail field. 248 - Calling the calc method to have the totals calculated. 249 250 Description of argument(s): 251 boot_type The type of boot test just done (e.g. "REST Power On"). 252 boot_status The status of the boot just done. This should be equal to either "pass" 253 or "fail" (case-insensitive). 254 """ 255 256 self.__boot_results.inc_row_field(boot_type, boot_status.lower()) 257 self.__boot_results.calc() 258 259 def sprint_report(self, header_footer="\n"): 260 r""" 261 String-print the formatted boot_resuls_table and return them. 262 263 Description of argument(s): 264 header_footer This indicates whether a header and footer are to be included in the 265 report. 266 """ 267 268 buffer = "" 269 270 buffer += gp.sprint(header_footer) 271 buffer += self.__boot_results.sprint_report() 272 buffer += gp.sprint(header_footer) 273 274 return buffer 275 276 def print_report(self, header_footer="\n", quiet=None): 277 r""" 278 Print the formatted boot_resuls_table to the console. 279 280 Description of argument(s): 281 See sprint_report for details. 282 quiet Only print if this value is 0. This function will search upward in the 283 stack to get the default value. 284 """ 285 286 quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) 287 288 gp.qprint(self.sprint_report(header_footer)) 289 290 def sprint_obj(self): 291 r""" 292 sprint the fields of this object. This would normally be for debug purposes only. 293 """ 294 295 buffer = "" 296 297 buffer += "class name: " + self.__class__.__name__ + "\n" 298 buffer += gp.sprint_var(self.__obj_name) 299 buffer += self.__boot_results.sprint_obj() 300 buffer += gp.sprint_var(self.__initial_boot_pass) 301 buffer += gp.sprint_var(self.__initial_boot_fail) 302 303 return buffer 304 305 def print_obj(self): 306 r""" 307 Print the fields of this object to stdout. This would normally be for debug purposes. 308 """ 309 310 gp.gp_print(self.sprint_obj()) 311 312 313def create_boot_results_file_path(pgm_name, openbmc_nickname, master_pid): 314 r""" 315 Create a file path to be used to store a boot_results object. 316 317 Description of argument(s): 318 pgm_name The name of the program. This will form part of the resulting file name. 319 openbmc_nickname The name of the system. This could be a nickname, a hostname, an IP, 320 etc. This will form part of the resulting file name. 321 master_pid The master process id which will form part of the file name. 322 """ 323 324 USER = os.environ.get("USER", "") 325 dir_path = "/tmp/" + USER + "/" 326 if not os.path.exists(dir_path): 327 os.makedirs(dir_path) 328 329 file_name_dict = vf.create_var_dict(pgm_name, openbmc_nickname, master_pid) 330 return vf.create_file_path( 331 file_name_dict, dir_path=dir_path, file_suffix=":boot_results" 332 ) 333 334 335def cleanup_boot_results_file(): 336 r""" 337 Delete all boot results files whose corresponding pids are no longer active. 338 """ 339 340 # Use create_boot_results_file_path to create a globex to find all of the existing boot results files. 341 globex = create_boot_results_file_path("*", "*", "*") 342 file_list = sorted(glob.glob(globex)) 343 for file_path in file_list: 344 # Use parse_file_path to extract info from the file path. 345 file_dict = vf.parse_file_path(file_path) 346 if gm.pid_active(file_dict["master_pid"]): 347 gp.qprint_timen("Preserving " + file_path + ".") 348 else: 349 gc.cmd_fnc("rm -f " + file_path) 350 351 352def update_boot_history(boot_history, boot_start_message, max_boot_history=10): 353 r""" 354 Update the boot_history list by appending the boot_start_message and by removing all but the last n 355 entries. 356 357 Description of argument(s): 358 boot_history A list of boot start messages. 359 boot_start_message This is typically a time-stamped line of text announcing the start of a 360 boot test. 361 max_boot_history The max number of entries to be kept in the boot_history list. The 362 oldest entries are deleted to achieve this list size. 363 """ 364 365 boot_history.append(boot_start_message) 366 367 # Trim list to max number of entries. 368 del boot_history[: max(0, len(boot_history) - max_boot_history)] 369 370 371def print_boot_history(boot_history, quiet=None): 372 r""" 373 Print the last ten boots done with their time stamps. 374 375 Description of argument(s): 376 quiet Only print if this value is 0. This function will search upward in the 377 stack to get the default value. 378 """ 379 380 quiet = int(gm.dft(quiet, gp.get_stack_var("quiet", 0))) 381 382 # indent 0, 90 chars wide, linefeed, char is "=" 383 gp.qprint_dashes(0, 90) 384 gp.qprintn("Last 10 boots:\n") 385 386 for boot_entry in boot_history: 387 gp.qprint(boot_entry) 388 gp.qprint_dashes(0, 90) 389