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