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