1#!/usr/bin/env python3
2
3r"""
4This script will parse error log yaml file(s) and generate
5a header file which will then be used by the error logging client and
6server to collect and validate the error information generated by the
7openbmc software components.
8
9This code uses a mako template to provide the basic template of the header
10file we're going to generate.  We then call it with information from the
11yaml to generate the header file.
12"""
13
14import os
15import sys
16from optparse import OptionParser
17
18from mako.template import Template
19
20import yaml
21
22
23def order_inherited_errors(i_errors, i_parents):
24    # the ordered list of errors
25    errors = list()
26    has_inheritance = False
27    for error in i_errors:
28        if i_parents[error] is not None:
29            has_inheritance = True
30            break
31
32    if has_inheritance:
33        # Order the error codes list such that an error is never placed
34        # before it's parent. This way generated code can ensure parent
35        # definitions precede child error definitions.
36        while len(errors) < len(i_errors):
37            for error in i_errors:
38                if error in errors:
39                    # already ordererd
40                    continue
41                if (not i_parents[error]) or (i_parents[error] in errors):
42                    # parent present, or has no parent, either way this error
43                    # can be added
44                    errors.append(error)
45    else:
46        # no inherited errors
47        errors = i_errors
48
49    return errors
50
51
52def check_error_inheritance(i_errors, i_parents):
53    for error in i_errors:
54        if i_parents[error] and (i_parents[error] not in i_errors):
55            print(
56                error
57                + " inherits "
58                + i_parents[error]
59                + " but the latter is not defined"
60            )
61            return False
62    return True
63
64
65# Return the yaml files with their directory structure plus the file name
66# without the yaml extension, which will be used to set the namespaces.
67# Ex: file xyz/openbmc_project/Error/Callout/Device.errors.yaml
68# will have namespce xyz/openbmc_project/Error/Callout/Device
69def get_error_yaml_files(i_yaml_dir, i_test_dir):
70    yaml_files = dict()
71    if i_yaml_dir != "None":
72        for root, dirs, files in os.walk(i_yaml_dir):
73            for files in [
74                file for file in files if file.endswith(".errors.yaml")
75            ]:
76                splitdir = root.split(i_yaml_dir)[1] + "/" + files[:-12]
77                if splitdir.startswith("/"):
78                    splitdir = splitdir[1:]
79                yaml_files[(os.path.join(root, files))] = splitdir
80    for root, dirs, files in os.walk(i_test_dir):
81        for files in [file for file in files if file.endswith(".errors.yaml")]:
82            splitdir = root.split(i_test_dir)[1] + "/" + files[:-12]
83            yaml_files[(os.path.join(root, files))] = splitdir
84    return yaml_files
85
86
87def get_meta_yaml_file(i_error_yaml_file):
88    # the meta data will be defined in file name where we replace
89    # <Interface>.errors.yaml with <Interface>.metadata.yaml
90    meta_yaml = i_error_yaml_file.replace("errors", "metadata")
91    return meta_yaml
92
93
94def get_cpp_type(i_type):
95    typeMap = {
96        "boolean": "bool",
97        "int8": "int8_t",
98        "int16": "int16_t",
99        "int32": "int32_t",
100        "int64": "int64_t",
101        "uint8": "uint8_t",
102        "uint16": "uint16_t",
103        "uint32": "uint32_t",
104        "uint64": "uint64_t",
105        "double": "double",
106        # const char* aids usage of constexpr
107        "string": "const char*",
108    }
109
110    return typeMap[i_type]
111
112
113def gen_elog_hpp(
114    i_yaml_dir, i_test_dir, i_output_hpp, i_template_dir, i_elog_mako
115):
116    r"""
117    Read  yaml file(s) under input yaml dir, grab the relevant data and call
118    the mako template to generate the output header file.
119
120    Description of arguments:
121    i_yaml_dir                  directory containing base error yaml files
122    i_test_dir                  directory containing test error yaml files
123    i_output_hpp                name of the to be generated output hpp
124    i_template_dir              directory containing error mako templates
125    i_elog_mako                 error mako template to render
126    """
127
128    # Input parameters to mako template
129    errors = list()  # Main error codes
130    error_msg = dict()  # Error msg that corresponds to error code
131    error_lvl = dict()  # Error code log level (debug, info, error, ...)
132    meta = dict()  # The meta data names associated (ERRNO, FILE_NAME, ...)
133    meta_data = dict()  # The meta data info (type, format)
134    parents = dict()
135    metadata_process = dict()  # metadata that have the 'process' keyword set
136
137    # Verify the input mako file
138    template_path = os.path.join(i_template_dir, i_elog_mako)
139    if not (os.path.isfile(template_path)):
140        print("Cannot find input template file " + template_path)
141        exit(1)
142    template_path = os.path.abspath(template_path)
143
144    error_yamls = get_error_yaml_files(i_yaml_dir, i_test_dir)
145
146    for error_yaml in error_yamls:
147        # Verify the error yaml file
148        if not (os.path.isfile(error_yaml)):
149            print("Cannot find input yaml file " + error_yaml)
150            exit(1)
151
152        # Verify the metadata yaml file
153        meta_yaml = get_meta_yaml_file(error_yaml)
154
155        get_elog_data(
156            error_yaml,
157            meta_yaml,
158            error_yamls[error_yaml],
159            # Last arg is a tuple
160            (
161                errors,
162                error_msg,
163                error_lvl,
164                meta,
165                meta_data,
166                parents,
167                metadata_process,
168            ),
169        )
170
171    if not check_error_inheritance(errors, parents):
172        print("Error - failed to validate error inheritance")
173        exit(1)
174
175    errors = order_inherited_errors(errors, parents)
176
177    # Load the mako template and call it with the required data
178    yaml_dir = i_yaml_dir.strip("./")
179    yaml_dir = yaml_dir.strip("../")
180    template = Template(filename=template_path)
181    f = open(i_output_hpp, "w")
182    f.write(
183        template.render(
184            errors=errors,
185            error_msg=error_msg,
186            error_lvl=error_lvl,
187            meta=meta,
188            meta_data=meta_data,
189            parents=parents,
190            metadata_process=metadata_process,
191        )
192    )
193    f.close()
194
195
196def get_elog_data(i_elog_yaml, i_elog_meta_yaml, i_namespace, o_elog_data):
197    r"""
198    Parse the error and metadata yaml files in order to pull out
199    error metadata.
200
201    Use default values if metadata yaml file is not found.
202
203    Description of arguments:
204    i_elog_yaml                 error yaml file
205    i_elog_meta_yaml            metadata yaml file
206    i_namespace                 namespace data
207    o_elog_data                 error metadata
208    """
209    (
210        errors,
211        error_msg,
212        error_lvl,
213        meta,
214        meta_data,
215        parents,
216        metadata_process,
217    ) = o_elog_data
218    ifile = yaml.safe_load(open(i_elog_yaml))
219
220    # for all the errors in error yaml file
221    for error in ifile:
222        if "name" not in error:
223            print(
224                "Error - Did not find name in entry %s in file %s "
225                % (str(error), i_elog_yaml)
226            )
227            exit(1)
228        fullname = i_namespace.replace("/", ".") + "." + error["name"]
229        errors.append(fullname)
230
231        if "description" in error:
232            error_msg[fullname] = error["description"].strip()
233
234        # set default values
235        error_lvl[fullname] = "ERR"
236        parents[fullname] = None
237
238        # check if meta data yaml file is found
239        if not os.path.isfile(i_elog_meta_yaml):
240            continue
241        mfile = yaml.safe_load(open(i_elog_meta_yaml))
242
243        # Find the meta data entry
244        match = None
245        for meta_entry in mfile:
246            if meta_entry["name"] == error["name"]:
247                match = meta_entry
248                break
249
250        if match is None:
251            continue
252
253        error_lvl[fullname] = match.get("level", "ERR")
254
255        # Get 0th inherited error (current support - single inheritance)
256        if "inherits" in match:
257            parents[fullname] = match["inherits"][0]
258
259        # Put all errors in meta[] even the meta is empty
260        # so that child errors could inherits such error without meta
261        tmp_meta = []
262        if "meta" in match:
263            # grab all the meta data fields and info
264            for i in match["meta"]:
265                str_short = i["str"].split("=")[0]
266                tmp_meta.append(str_short)
267                meta_data[str_short] = {}
268                meta_data[str_short]["str"] = i["str"]
269                meta_data[str_short]["str_short"] = str_short
270                meta_data[str_short]["type"] = get_cpp_type(i["type"])
271                if ("process" in i) and (i["process"] is True):
272                    metadata_process[str_short] = fullname + "." + str_short
273        meta[fullname] = tmp_meta
274
275    # Debug
276    # for i in errors:
277    #   print "ERROR: " + errors[i]
278    #   print " MSG:  " + error_msg[errors[i]]
279    #   print " LVL:  " + error_lvl[errors[i]]
280    #   print " META: "
281    #   print meta[i]
282
283
284def main(i_args):
285    parser = OptionParser()
286
287    parser.add_option(
288        "-m",
289        "--mako",
290        dest="elog_mako",
291        default="elog-gen-template.mako.hpp",
292        help="input mako template file to use",
293    )
294
295    parser.add_option(
296        "-o",
297        "--output",
298        dest="output_hpp",
299        default="elog-errors.hpp",
300        help="output hpp to generate, elog-errors.hpp default",
301    )
302
303    parser.add_option(
304        "-y",
305        "--yamldir",
306        dest="yamldir",
307        default="None",
308        help="Base directory of yaml files to process",
309    )
310
311    parser.add_option(
312        "-u",
313        "--testdir",
314        dest="testdir",
315        default="./tools/example/",
316        help="Unit test directory of yaml files to process",
317    )
318
319    parser.add_option(
320        "-t",
321        "--templatedir",
322        dest="templatedir",
323        default="phosphor-logging/templates/",
324        help="Base directory of files to process",
325    )
326
327    (options, args) = parser.parse_args(i_args)
328
329    gen_elog_hpp(
330        options.yamldir,
331        options.testdir,
332        options.output_hpp,
333        options.templatedir,
334        options.elog_mako,
335    )
336
337
338# Only run if it's a script
339if __name__ == "__main__":
340    main(sys.argv[1:])
341