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 json
26import os
27
28import yaml
29
30
31def get_errors(yaml_dirs):
32    """Finds all of the errors in all of the error YAML files in
33    the directories passed in."""
34
35    all_errors = []
36    for yaml_dir in yaml_dirs:
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    if os.path.exists(yaml_dir):
121        for directory, _, files in os.walk(yaml_dir):
122            if not files:
123                continue
124
125            err_files += [
126                os.path.relpath(os.path.join(directory, f), yaml_dir)
127                for f in [f for f in files if f.endswith(".errors.yaml")]
128            ]
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(
161        "\n%d total errors (with %d total details blocks) in the "
162        "policy table\n\n" % (len(policy_errors), num_details)
163    )
164
165
166if __name__ == "__main__":
167    parser = argparse.ArgumentParser(description="Error log policy reports")
168
169    parser.add_argument(
170        "-y",
171        "--yaml_dirs",
172        dest="yaml_dirs",
173        default=".",
174        help="Comma separated list of error YAML dirs",
175    )
176    parser.add_argument(
177        "-e",
178        "--error_file",
179        dest="error_file",
180        default="obmc-errors.json",
181        help="Output Error report file",
182    )
183    parser.add_argument(
184        "-p",
185        "--policy",
186        dest="policy_file",
187        default="condensed.json",
188        help="Condensed policy in JSON",
189    )
190    parser.add_argument(
191        "-x",
192        "--crosscheck",
193        dest="crosscheck_file",
194        help="YAML vs policy table crosscheck output file",
195    )
196
197    args = parser.parse_args()
198
199    dirs = args.yaml_dirs.split(",")
200    errors = get_errors(dirs)
201
202    with open(args.error_file, "w") as outfile:
203        json.dump(
204            errors, outfile, sort_keys=True, indent=2, separators=(",", ":")
205        )
206
207    if args.crosscheck_file:
208        with open(args.policy_file) as pf:
209            policy = yaml.safe_load(pf.read())
210
211        crosscheck(errors, policy, args.crosscheck_file)
212