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