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