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