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