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