xref: /openbmc/bmcweb/scripts/parse_registries.py (revision 4aad6ed2fbe6cb2c5b652a5f25c2eabecb53746d)
1#!/usr/bin/env python3
2import argparse
3import json
4import os
5from collections import OrderedDict
6
7import requests
8
9PRAGMA_ONCE = """#pragma once
10"""
11
12WARNING = """/****************************************************************
13 *                 READ THIS WARNING FIRST
14 * This is an auto-generated header which contains definitions
15 * for Redfish DMTF defined messages.
16 * DO NOT modify this registry outside of running the
17 * parse_registries.py script.  The definitions contained within
18 * this file are owned by DMTF.  Any modifications to these files
19 * should be first pushed to the relevant registry in the DMTF
20 * github organization.
21 ***************************************************************/"""
22
23COPYRIGHT = """// SPDX-License-Identifier: Apache-2.0
24// SPDX-FileCopyrightText: Copyright OpenBMC Authors
25"""
26
27INCLUDES = """
28#include "registries.hpp"
29
30#include <array>
31
32// clang-format off
33
34namespace redfish::registries::{}
35{{
36"""
37
38REGISTRY_HEADER = f"{COPYRIGHT}{PRAGMA_ONCE}{WARNING}{INCLUDES}"
39
40SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
41
42include_path = os.path.realpath(
43    os.path.join(SCRIPT_DIR, "..", "redfish-core", "include", "registries")
44)
45
46proxies = {"https": os.environ.get("https_proxy", None)}
47
48
49def make_getter(dmtf_name, header_name, type_name):
50    url = "https://redfish.dmtf.org/registries/{}".format(dmtf_name)
51    dmtf = requests.get(url, proxies=proxies)
52    dmtf.raise_for_status()
53    json_file = json.loads(dmtf.text, object_pairs_hook=OrderedDict)
54    path = os.path.join(include_path, header_name)
55    return (path, json_file, type_name, url)
56
57
58def openbmc_local_getter():
59    url = "https://raw.githubusercontent.com/openbmc/bmcweb/refs/heads/master/redfish-core/include/registries/openbmc.json"
60    with open(
61        os.path.join(
62            SCRIPT_DIR,
63            "..",
64            "redfish-core",
65            "include",
66            "registries",
67            "openbmc.json",
68        ),
69        "rb",
70    ) as json_file:
71        json_file = json.load(json_file)
72
73    path = os.path.join(include_path, "openbmc_message_registry.hpp")
74    return (path, json_file, "openbmc", url)
75
76
77def update_registries(files):
78    # Remove the old files
79    for file, json_dict, namespace, url in files:
80        try:
81            os.remove(file)
82        except BaseException:
83            print("{} not found".format(file))
84
85        with open(file, "w") as registry:
86
87            version_split = json_dict["RegistryVersion"].split(".")
88
89            registry.write(REGISTRY_HEADER.format(namespace))
90            # Parse the Registry header info
91            registry.write(
92                "const Header header = {{\n"
93                '    "{json_dict[@Redfish.Copyright]}",\n'
94                '    "{json_dict[@odata.type]}",\n'
95                "    {version_split[0]},\n"
96                "    {version_split[1]},\n"
97                "    {version_split[2]},\n"
98                '    "{json_dict[Name]}",\n'
99                '    "{json_dict[Language]}",\n'
100                '    "{json_dict[Description]}",\n'
101                '    "{json_dict[RegistryPrefix]}",\n'
102                '    "{json_dict[OwningEntity]}",\n'
103                "}};\n"
104                "constexpr const char* url =\n"
105                '    "{url}";\n'
106                "\n"
107                "constexpr std::array registry =\n"
108                "{{\n".format(
109                    json_dict=json_dict,
110                    url=url,
111                    version_split=version_split,
112                )
113            )
114
115            messages_sorted = sorted(json_dict["Messages"].items())
116            for messageId, message in messages_sorted:
117                registry.write(
118                    "    MessageEntry{{\n"
119                    '        "{messageId}",\n'
120                    "        {{\n"
121                    '            "{message[Description]}",\n'
122                    '            "{message[Message]}",\n'
123                    '            "{message[MessageSeverity]}",\n'
124                    "            {message[NumberOfArgs]},\n"
125                    "            {{".format(
126                        messageId=messageId, message=message
127                    )
128                )
129                paramTypes = message.get("ParamTypes")
130                if paramTypes:
131                    for paramType in paramTypes:
132                        registry.write(
133                            '\n                "{}",'.format(paramType)
134                        )
135                    registry.write("\n            },\n")
136                else:
137                    registry.write("},\n")
138                registry.write(
139                    '            "{message[Resolution]}",\n'
140                    "        }}}},\n".format(message=message)
141                )
142
143            registry.write("\n};\n\nenum class Index\n{\n")
144            for index, (messageId, message) in enumerate(messages_sorted):
145                messageId = messageId[0].lower() + messageId[1:]
146                registry.write("    {} = {},\n".format(messageId, index))
147            registry.write(
148                "}};\n}} // namespace redfish::registries::{}\n".format(
149                    namespace
150                )
151            )
152
153
154def get_privilege_string_from_list(privilege_list):
155    privilege_string = "{{\n"
156    for privilege_json in privilege_list:
157        privileges = privilege_json["Privilege"]
158        privilege_string += "    {"
159        for privilege in privileges:
160            if privilege == "NoAuth":
161                continue
162            privilege_string += '"'
163            privilege_string += privilege
164            privilege_string += '",\n'
165        if privilege != "NoAuth":
166            privilege_string = privilege_string[:-2]
167        privilege_string += "}"
168        privilege_string += ",\n"
169    privilege_string = privilege_string[:-2]
170    privilege_string += "\n}}"
171    return privilege_string
172
173
174def get_variable_name_for_privilege_set(privilege_list):
175    names = []
176    for privilege_json in privilege_list:
177        privileges = privilege_json["Privilege"]
178        names.append("And".join(privileges))
179    return "Or".join(names)
180
181
182PRIVILEGE_HEADER = (
183    COPYRIGHT
184    + PRAGMA_ONCE
185    + WARNING
186    + """
187#include "privileges.hpp"
188
189#include <array>
190
191// clang-format off
192
193namespace redfish::privileges
194{
195"""
196)
197
198
199def get_response_code(entry_id, entry):
200    codes = {
201        "InternalError": "internal_server_error",
202        "OperationTimeout": "internal_server_error",
203        "PropertyValueResourceConflict": "conflict",
204        "ResourceInUse": "service_unavailable",
205        "ServiceTemporarilyUnavailable": "service_unavailable",
206        "ResourceCannotBeDeleted": "method_not_allowed",
207        "PropertyValueModified": "ok",
208        "InsufficientPrivilege": "forbidden",
209        "AccountForSessionNoLongerExists": "forbidden",
210        "ServiceDisabled": "service_unavailable",
211        "ServiceInUnknownState": "service_unavailable",
212        "EventSubscriptionLimitExceeded": "service_unavailable",
213        "ResourceAtUriUnauthorized": "unauthorized",
214        "SessionTerminated": "ok",
215        "SubscriptionTerminated": "ok",
216        "PropertyNotWritable": "forbidden",
217        "MaximumErrorsExceeded": "internal_server_error",
218        "GeneralError": "internal_server_error",
219        "PreconditionFailed": "precondition_failed",
220        "OperationFailed": "bad_gateway",
221        "ServiceShuttingDown": "service_unavailable",
222        "AccountRemoved": "ok",
223        "PropertyValueExternalConflict": "conflict",
224        "InsufficientStorage": "insufficient_storage",
225        "OperationNotAllowed": "method_not_allowed",
226        "ResourceNotFound": "not_found",
227        "CouldNotEstablishConnection": "not_found",
228        "AccessDenied": "forbidden",
229        "Success": None,
230        "Created": "created",
231        "NoValidSession": "forbidden",
232        "SessionLimitExceeded": "service_unavailable",
233        "ResourceExhaustion": "service_unavailable",
234        "AccountModified": "ok",
235        "PasswordChangeRequired": None,
236        "ResourceInStandby": "service_unavailable",
237        "GenerateSecretKeyRequired": "forbidden",
238    }
239
240    code = codes.get(entry_id, "NOCODE")
241    if code != "NOCODE":
242        return code
243
244    return "bad_request"
245
246
247def make_error_function(
248    entry_id, entry, is_header, registry_name, namespace_name
249):
250    arg_nonstring_types = {
251        "const boost::urls::url_view_base&": {
252            "AccessDenied": [1],
253            "CouldNotEstablishConnection": [1],
254            "GenerateSecretKeyRequired": [1],
255            "InvalidObject": [1],
256            "PasswordChangeRequired": [1],
257            "PropertyValueResourceConflict": [3],
258            "ResetRequired": [1],
259            "ResourceAtUriInUnknownFormat": [1],
260            "ResourceAtUriUnauthorized": [1],
261            "ResourceCreationConflict": [1],
262            "ResourceMissingAtURI": [1],
263            "SourceDoesNotSupportProtocol": [1],
264        },
265        "const nlohmann::json&": {
266            "ActionParameterValueError": [1],
267            "ActionParameterValueFormatError": [1],
268            "ActionParameterValueTypeError": [1],
269            "PropertyValueExternalConflict": [2],
270            "PropertyValueFormatError": [1],
271            "PropertyValueIncorrect": [2],
272            "PropertyValueModified": [2],
273            "PropertyValueNotInList": [1],
274            "PropertyValueOutOfRange": [1],
275            "PropertyValueResourceConflict": [2],
276            "PropertyValueTypeError": [1],
277            "QueryParameterValueFormatError": [1],
278            "QueryParameterValueTypeError": [1],
279        },
280        "uint64_t": {
281            "ArraySizeTooLong": [2],
282            "InvalidIndex": [1],
283            "StringValueTooLong": [2],
284            "TaskProgressChanged": [2],
285        },
286    }
287
288    out = ""
289    args = []
290    argtypes = []
291    for arg_index, arg in enumerate(entry.get("ParamTypes", [])):
292        arg_index += 1
293        typename = "std::string_view"
294        for typestring, entries in arg_nonstring_types.items():
295            if arg_index in entries.get(entry_id, []):
296                typename = typestring
297
298        argtypes.append(typename)
299        args.append(f"{typename} arg{arg_index}")
300    function_name = entry_id[0].lower() + entry_id[1:]
301    arg = ", ".join(args)
302    out += f"nlohmann::json {function_name}({arg})"
303
304    if is_header:
305        out += ";\n\n"
306    else:
307        out += "\n{\n"
308        to_array_type = ""
309        if argtypes:
310            outargs = []
311            for index, typename in enumerate(argtypes):
312                index += 1
313                if typename == "const nlohmann::json&":
314                    out += f"std::string arg{index}Str = arg{index}.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace);\n"
315                elif typename == "uint64_t":
316                    out += f"std::string arg{index}Str = std::to_string(arg{index});\n"
317
318            for index, typename in enumerate(argtypes):
319                index += 1
320                if typename == "const boost::urls::url_view_base&":
321                    outargs.append(f"arg{index}.buffer()")
322                    to_array_type = "<std::string_view>"
323                elif typename == "const nlohmann::json&":
324                    outargs.append(f"arg{index}Str")
325                    to_array_type = "<std::string_view>"
326                elif typename == "uint64_t":
327                    outargs.append(f"arg{index}Str")
328                    to_array_type = "<std::string_view>"
329                else:
330                    outargs.append(f"arg{index}")
331            argstring = ", ".join(outargs)
332
333        if argtypes:
334            arg_param = f"std::to_array{to_array_type}({{{argstring}}})"
335        else:
336            arg_param = "{}"
337        out += f"    return getLog(redfish::registries::{namespace_name}::Index::{function_name}, {arg_param});"
338        out += "\n}\n\n"
339    if registry_name == "Base":
340        args.insert(0, "crow::Response& res")
341        if entry_id == "InternalError":
342            if is_header:
343                args.append(
344                    "std::source_location location = std::source_location::current()"
345                )
346            else:
347                args.append("const std::source_location location")
348        arg = ", ".join(args)
349        out += f"void {function_name}({arg})"
350        if is_header:
351            out += ";\n"
352        else:
353            out += "\n{\n"
354            if entry_id == "InternalError":
355                out += """BMCWEB_LOG_CRITICAL("Internal Error {}({}:{}) `{}`: ", location.file_name(),
356                            location.line(), location.column(),
357                            location.function_name());\n"""
358
359            if entry_id == "ServiceTemporarilyUnavailable":
360                out += "res.addHeader(boost::beast::http::field::retry_after, arg1);"
361
362            res = get_response_code(entry_id, entry)
363            if res:
364                out += f"    res.result(boost::beast::http::status::{res});\n"
365            args_out = ", ".join([f"arg{x+1}" for x in range(len(argtypes))])
366
367            addMessageToJson = {
368                "PropertyDuplicate": 1,
369                "ResourceAlreadyExists": 2,
370                "CreateFailedMissingReqProperties": 1,
371                "PropertyValueFormatError": 2,
372                "PropertyValueNotInList": 2,
373                "PropertyValueTypeError": 2,
374                "PropertyValueError": 1,
375                "PropertyNotWritable": 1,
376                "PropertyValueModified": 1,
377                "PropertyMissing": 1,
378            }
379
380            addMessageToRoot = [
381                "SessionTerminated",
382                "SubscriptionTerminated",
383                "AccountRemoved",
384                "Created",
385                "Success",
386                "PasswordChangeRequired",
387            ]
388
389            if entry_id in addMessageToJson:
390                out += f"    addMessageToJson(res.jsonValue, {function_name}({args_out}), arg{addMessageToJson[entry_id]});\n"
391            elif entry_id in addMessageToRoot:
392                out += f"    addMessageToJsonRoot(res.jsonValue, {function_name}({args_out}));\n"
393            else:
394                out += f"    addMessageToErrorJson(res.jsonValue, {function_name}({args_out}));\n"
395            out += "}\n"
396    out += "\n"
397    return out
398
399
400def create_error_registry(
401    entry, registry_version, registry_name, namespace_name, filename
402):
403    file, json_dict, namespace, url = entry
404    base_filename = filename + "_messages"
405
406    error_messages_hpp = os.path.join(
407        SCRIPT_DIR, "..", "redfish-core", "include", f"{base_filename}.hpp"
408    )
409    messages = json_dict["Messages"]
410
411    with open(
412        error_messages_hpp,
413        "w",
414    ) as out:
415        out.write(PRAGMA_ONCE)
416        out.write(WARNING)
417        out.write(
418            """
419// These generated headers are a superset of what is needed.
420// clang sees them as an error, so ignore
421// NOLINTBEGIN(misc-include-cleaner)
422#include "http_response.hpp"
423
424#include <boost/url/url_view_base.hpp>
425#include <nlohmann/json.hpp>
426
427#include <cstdint>
428#include <source_location>
429#include <string_view>
430// NOLINTEND(misc-include-cleaner)
431
432namespace redfish
433{
434
435namespace messages
436{
437"""
438        )
439        for entry_id, entry in messages.items():
440            message = entry["Message"]
441            for index in range(1, 10):
442                message = message.replace(f"'%{index}'", f"<arg{index}>")
443                message = message.replace(f"%{index}", f"<arg{index}>")
444
445            if registry_name == "Base":
446                out.write("/**\n")
447                out.write(f"* @brief Formats {entry_id} message into JSON\n")
448                out.write(f'* Message body: "{message}"\n')
449                out.write("*\n")
450                arg_index = 0
451                for arg_index, arg in enumerate(entry.get("ParamTypes", [])):
452                    arg_index += 1
453
454                    out.write(
455                        f"* @param[in] arg{arg_index} Parameter of message that will replace %{arg_index} in its body.\n"
456                    )
457                out.write("*\n")
458                out.write(
459                    f"* @returns Message {entry_id} formatted to JSON */\n"
460                )
461
462            out.write(
463                make_error_function(
464                    entry_id, entry, True, registry_name, namespace_name
465                )
466            )
467        out.write("    }\n")
468        out.write("}\n")
469
470    error_messages_cpp = os.path.join(
471        SCRIPT_DIR, "..", "redfish-core", "src", f"{base_filename}.cpp"
472    )
473    with open(
474        error_messages_cpp,
475        "w",
476    ) as out:
477        out.write(WARNING)
478        out.write(f'\n#include "{base_filename}.hpp"\n')
479        headers = []
480
481        headers.append('"registries.hpp"')
482        if registry_name == "Base":
483            reg_name_lower = "base"
484            headers.append('"error_message_utils.hpp"')
485            headers.append('"http_response.hpp"')
486            headers.append('"logging.hpp"')
487            headers.append("<boost/beast/http/field.hpp>")
488            headers.append("<boost/beast/http/status.hpp>")
489            headers.append("<boost/url/url_view_base.hpp>")
490            headers.append("<source_location>")
491        else:
492            reg_name_lower = namespace_name.lower()
493        headers.append(f'"registries/{reg_name_lower}_message_registry.hpp"')
494
495        headers.append("<nlohmann/json.hpp>")
496        headers.append("<array>")
497        headers.append("<cstddef>")
498        headers.append("<span>")
499
500        if registry_name not in ("ResourceEvent", "HeartbeatEvent"):
501            headers.append("<cstdint>")
502            headers.append("<string>")
503        headers.append("<string_view>")
504
505        for header in headers:
506            out.write(f"#include {header}\n")
507
508        out.write(
509            """
510// Clang can't seem to decide whether this header needs to be included or not,
511// and is inconsistent.  Include it for now
512// NOLINTNEXTLINE(misc-include-cleaner)
513#include <utility>
514
515namespace redfish
516{
517
518namespace messages
519{
520"""
521        )
522        out.write(
523            """
524static nlohmann::json getLog(redfish::registries::{namespace_name}::Index name,
525                             std::span<const std::string_view> args)
526{{
527    size_t index = static_cast<size_t>(name);
528    if (index >= redfish::registries::{namespace_name}::registry.size())
529    {{
530        return {{}};
531    }}
532    return getLogFromRegistry(redfish::registries::{namespace_name}::header,
533                              redfish::registries::{namespace_name}::registry, index, args);
534}}
535
536""".format(
537                namespace_name=namespace_name
538            )
539        )
540        for entry_id, entry in messages.items():
541            out.write(
542                f"""/**
543 * @internal
544 * @brief Formats {entry_id} message into JSON
545 *
546 * See header file for more information
547 * @endinternal
548 */
549"""
550            )
551            message = entry["Message"]
552            out.write(
553                make_error_function(
554                    entry_id, entry, False, registry_name, namespace_name
555                )
556            )
557
558        out.write("    }\n")
559        out.write("}\n")
560    os.system(f"clang-format -i {error_messages_hpp} {error_messages_cpp}")
561
562
563def make_privilege_registry():
564    path, json_file, type_name, url = make_getter(
565        "Redfish_1.5.0_PrivilegeRegistry.json",
566        "privilege_registry.hpp",
567        "privilege",
568    )
569    with open(path, "w") as registry:
570        registry.write(PRIVILEGE_HEADER)
571
572        privilege_dict = {}
573        for mapping in json_file["Mappings"]:
574            # first pass, identify all the unique privilege sets
575            for operation, privilege_list in mapping["OperationMap"].items():
576                privilege_dict[
577                    get_privilege_string_from_list(privilege_list)
578                ] = (privilege_list,)
579        for index, key in enumerate(privilege_dict):
580            (privilege_list,) = privilege_dict[key]
581            name = get_variable_name_for_privilege_set(privilege_list)
582            registry.write(
583                "const std::array<Privileges, {length}> "
584                "privilegeSet{name} = {key};\n".format(
585                    length=len(privilege_list), name=name, key=key
586                )
587            )
588            privilege_dict[key] = (privilege_list, name)
589
590        for mapping in json_file["Mappings"]:
591            entity = mapping["Entity"]
592            registry.write("// {}\n".format(entity))
593            for operation, privilege_list in mapping["OperationMap"].items():
594                privilege_string = get_privilege_string_from_list(
595                    privilege_list
596                )
597                operation = operation.lower()
598
599                registry.write(
600                    "const static auto& {}{} = privilegeSet{};\n".format(
601                        operation, entity, privilege_dict[privilege_string][1]
602                    )
603                )
604            registry.write("\n")
605        registry.write(
606            "} // namespace redfish::privileges\n// clang-format on\n"
607        )
608
609
610def to_pascal_case(text):
611    s = text.replace("_", " ")
612    s = s.split()
613    if len(text) == 0:
614        return text
615    return "".join(i.capitalize() for i in s[0:])
616
617
618def main():
619    dmtf_registries = OrderedDict(
620        [
621            ("base", "1.19.0"),
622            ("composition", "1.1.2"),
623            ("environmental", "1.0.1"),
624            ("ethernet_fabric", "1.0.1"),
625            ("fabric", "1.0.2"),
626            ("heartbeat_event", "1.0.1"),
627            ("job_event", "1.0.1"),
628            ("license", "1.0.3"),
629            ("log_service", "1.0.1"),
630            ("network_device", "1.0.3"),
631            ("platform", "1.0.1"),
632            ("power", "1.0.1"),
633            ("resource_event", "1.3.0"),
634            ("sensor_event", "1.0.1"),
635            ("storage_device", "1.2.1"),
636            ("task_event", "1.0.3"),
637            ("telemetry", "1.0.0"),
638            ("update", "1.0.2"),
639        ]
640    )
641
642    parser = argparse.ArgumentParser()
643    parser.add_argument(
644        "--registries",
645        type=str,
646        default="privilege,openbmc,"
647        + ",".join([dmtf for dmtf in dmtf_registries]),
648        help="Comma delimited list of registries to update",
649    )
650
651    args = parser.parse_args()
652
653    registries = set(args.registries.split(","))
654    files = []
655    registries_map = OrderedDict()
656
657    for registry, version in dmtf_registries.items():
658        if registry in registries:
659            registry_pascal_case = to_pascal_case(registry)
660            files.append(
661                make_getter(
662                    f"{registry_pascal_case}.{version}.json",
663                    f"{registry}_message_registry.hpp",
664                    registry,
665                )
666            )
667            registries_map[registry] = files[-1]
668    if "openbmc" in registries:
669        files.append(openbmc_local_getter())
670
671    update_registries(files)
672
673    if "base" in registries_map:
674        create_error_registry(
675            registries_map["base"],
676            dmtf_registries["base"],
677            "Base",
678            "base",
679            "error",
680        )
681    if "heartbeat_event" in registries_map:
682        create_error_registry(
683            registries_map["heartbeat_event"],
684            dmtf_registries["heartbeat_event"],
685            "HeartbeatEvent",
686            "heartbeat_event",
687            "heartbeat",
688        )
689    if "resource_event" in registries_map:
690        create_error_registry(
691            registries_map["resource_event"],
692            dmtf_registries["resource_event"],
693            "ResourceEvent",
694            "resource_event",
695            "resource",
696        )
697    if "task_event" in registries_map:
698        create_error_registry(
699            registries_map["task_event"],
700            dmtf_registries["task_event"],
701            "TaskEvent",
702            "task_event",
703            "task",
704        )
705
706    if "privilege" in registries:
707        make_privilege_registry()
708
709
710if __name__ == "__main__":
711    main()
712