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