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