1#!/usr/bin/env python3
2
3'''Generates 2 reports about OpenBMC error logs:
4
5    1) Dumps every error defined in the errors.yaml files passed in
6       into a single JSON file that looks like:
7
8       {
9         "desc":"Callout IIC device",
10         "error":"xyz.openbmc_project.Common.Callout.Error.IIC",
11         "file":"xyz/openbmc_project/Common/Callout.errors.yaml",
12         "metadata":[
13           "CALLOUT_IIC_BUS",
14           "CALLOUT_IIC_ADDR",
15           "Inherits xyz.openbmc_project.Common.Callout.Error.Device"
16          ]
17       }
18
19    2) Crosschecks this generated JSON with the IBM error policy table,
20       showing if any errors are in one file but not the other.
21
22'''
23
24import argparse
25import os
26import json
27import yaml
28
29
30def get_errors(yaml_dirs):
31    '''Finds all of the errors in all of the error YAML files in
32       the directories passed in.'''
33
34    all_errors = []
35    for yaml_dir in yaml_dirs:
36        error_data = []
37        yaml_files = get_yaml(yaml_dir)
38
39        for yaml_file in yaml_files:
40            all_errors += read_error_yaml(yaml_dir, yaml_file)
41
42    return all_errors
43
44
45def read_error_yaml(yaml_dir, yaml_file):
46    '''Returns a list of dictionary objects reflecting the error YAML.'''
47
48    all_errors = []
49
50    #xyz/openbmc_project/x.errors.yaml -> xyz.openbmc_project.x.Error
51    error_base = yaml_file.replace(os.sep, '.')
52    error_base = error_base.replace('.errors.yaml', '')
53    error_base += '.Error.'
54
55    #Also needs to look up the metadata from the .metadata.yaml files
56    metadata_file = yaml_file.replace('errors.yaml', 'metadata.yaml')
57    metadata = []
58
59    if os.path.exists(os.path.join(yaml_dir, metadata_file)):
60        with open(os.path.join(yaml_dir, metadata_file)) as mfd:
61            metadata = yaml.safe_load(mfd.read())
62
63    with open(os.path.join(yaml_dir, yaml_file)) as fd:
64        data = yaml.safe_load(fd.read())
65
66        for e in data:
67            error = {}
68            error['error'] = error_base + e['name']
69            error['desc'] = e['description']
70            error['metadata'] = get_metadata(e['name'], metadata)
71            error['file'] = yaml_file
72            all_errors.append(error)
73
74    return all_errors
75
76
77def add_error(val):
78    '''Adds the '.Error' before the last segment of an error string.'''
79    dot = val.rfind('.')
80    return val[:dot] + '.Error' + val[dot:]
81
82
83def get_metadata(name, metadata):
84    '''Finds metadata entries for the error in the metadata
85       dictionary parsed out of the *.metadata.yaml files.
86
87       The metadata YAML looks something like:
88            - name: SlaveDetectionFailure
89              meta:
90                - str: "ERRNO=%d"
91                  type: int32
92              inherits:
93                - xyz.openbmc_project.Callout
94      '''
95
96    data = []
97    for m in metadata:
98        if m['name'] == name:
99            if 'meta' in m:
100                for entry in m['meta']:
101                    #Get the name from name=value
102                    n = entry['str'].split('=')[0]
103                    data.append(n)
104
105            #inherits is a list, return it comma separated
106            if 'inherits' in m:
107                vals = list(map(add_error, m['inherits']))
108                i = ','.join(vals)
109                data.append("Inherits %s" % i)
110
111    return data
112
113
114def get_yaml(yaml_dir):
115    '''Finds all of the *.errors.yaml files in the directory passed in.
116       Returns a list of entries like xyz/openbmc_project/Common.Errors.yaml.
117    '''
118
119    err_files = []
120    metadata_files = []
121    if os.path.exists(yaml_dir):
122        for directory, _, files in os.walk(yaml_dir):
123            if not files:
124                continue
125
126            err_files += [os.path.relpath(
127                    os.path.join(directory, f), yaml_dir)
128                    for f in [f for f in files if f.endswith('.errors.yaml')]]
129
130    return err_files
131
132
133def crosscheck(errors, policy, outfile):
134    '''Crosschecks that the errors found in the YAML are in the
135       policy file, and vice versa.
136    '''
137
138    policy_errors = [x['err'] for x in policy]
139    yaml_errors = [x['error'] for x in errors]
140
141    out = open(outfile, 'w')
142    out.write("YAML errors not in policy table:\n\n")
143
144    for e in yaml_errors:
145        if e not in policy_errors:
146            out.write("    %s\n" % e)
147
148    out.write("\n%d total errors in the YAML\n\n" % len(yaml_errors))
149    out.write("Policy errors not in YAML:\n\n")
150
151    for e in policy_errors:
152        if e not in yaml_errors:
153            out.write("    %s\n" % e)
154
155    num_details = 0
156    for e in policy:
157        for d in e['dtls']:
158            num_details += 1
159
160    out.write("\n%d total errors (with %d total details blocks) in the "
161              "policy table\n\n" % (len(policy_errors), num_details))
162
163if __name__ == '__main__':
164
165    parser = argparse.ArgumentParser(description='Error log policy reports')
166
167    parser.add_argument('-y', '--yaml_dirs',
168                        dest='yaml_dirs',
169                        default='.',
170                        help='Comma separated list of error YAML dirs')
171    parser.add_argument('-e', '--error_file',
172                        dest='error_file',
173                        default='obmc-errors.json',
174                        help='Output Error report file')
175    parser.add_argument('-p', '--policy',
176                        dest='policy_file',
177                        default='condensed.json',
178                        help='Condensed policy in JSON')
179    parser.add_argument('-x', '--crosscheck',
180                        dest='crosscheck_file',
181                        help='YAML vs policy table crosscheck output file')
182
183    args = parser.parse_args()
184
185    dirs = args.yaml_dirs.split(',')
186    errors = get_errors(dirs)
187
188    with open(args.error_file, 'w') as outfile:
189        json.dump(errors, outfile, sort_keys=True,
190                  indent=2, separators=(',', ':'))
191
192    if args.crosscheck_file:
193        with open(args.policy_file) as pf:
194            policy = yaml.safe_load(pf.read())
195
196        crosscheck(errors, policy, args.crosscheck_file)
197