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