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