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