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