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