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