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