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