blob: 20aac87def71557f35f78428b4f4d4d502116bad [file] [log] [blame]
#!/usr/bin/env python3
import argparse
import json
import os
from collections import OrderedDict
import requests
PRAGMA_ONCE = """#pragma once
"""
WARNING = """/****************************************************************
* READ THIS WARNING FIRST
* This is an auto-generated header which contains definitions
* for Redfish DMTF defined messages.
* DO NOT modify this registry outside of running the
* parse_registries.py script. The definitions contained within
* this file are owned by DMTF. Any modifications to these files
* should be first pushed to the relevant registry in the DMTF
* github organization.
***************************************************************/"""
REGISTRY_HEADER = (
PRAGMA_ONCE
+ WARNING
+ """
#include "registries.hpp"
#include <array>
// clang-format off
namespace redfish::registries::{}
{{
"""
)
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
include_path = os.path.realpath(
os.path.join(SCRIPT_DIR, "..", "redfish-core", "include", "registries")
)
proxies = {"https": os.environ.get("https_proxy", None)}
def make_getter(dmtf_name, header_name, type_name):
url = "https://redfish.dmtf.org/registries/{}".format(dmtf_name)
dmtf = requests.get(url, proxies=proxies)
dmtf.raise_for_status()
json_file = json.loads(dmtf.text, object_pairs_hook=OrderedDict)
path = os.path.join(include_path, header_name)
return (path, json_file, type_name, url)
def openbmc_local_getter():
url = "https://github.com/openbmc/bmcweb/blob/master/redfish-core/include/registries/openbmc.json"
with open(
os.path.join(
SCRIPT_DIR,
"..",
"redfish-core",
"include",
"registries",
"openbmc.json",
),
"rb",
) as json_file:
json_file = json.load(json_file)
path = os.path.join(include_path, "openbmc_message_registry.hpp")
return (path, json_file, "openbmc", url)
def update_registries(files):
# Remove the old files
for file, json_dict, namespace, url in files:
try:
os.remove(file)
except BaseException:
print("{} not found".format(file))
with open(file, "w") as registry:
version_split = json_dict["RegistryVersion"].split(".")
registry.write(REGISTRY_HEADER.format(namespace))
# Parse the Registry header info
registry.write(
"const Header header = {{\n"
' "{json_dict[@Redfish.Copyright]}",\n'
' "{json_dict[@odata.type]}",\n'
" {version_split[0]},\n"
" {version_split[1]},\n"
" {version_split[2]},\n"
' "{json_dict[Name]}",\n'
' "{json_dict[Language]}",\n'
' "{json_dict[Description]}",\n'
' "{json_dict[RegistryPrefix]}",\n'
' "{json_dict[OwningEntity]}",\n'
"}};\n"
"constexpr const char* url =\n"
' "{url}";\n'
"\n"
"constexpr std::array registry =\n"
"{{\n".format(
json_dict=json_dict,
url=url,
version_split=version_split,
)
)
messages_sorted = sorted(json_dict["Messages"].items())
for messageId, message in messages_sorted:
registry.write(
" MessageEntry{{\n"
' "{messageId}",\n'
" {{\n"
' "{message[Description]}",\n'
' "{message[Message]}",\n'
' "{message[MessageSeverity]}",\n'
" {message[NumberOfArgs]},\n"
" {{".format(
messageId=messageId, message=message
)
)
paramTypes = message.get("ParamTypes")
if paramTypes:
for paramType in paramTypes:
registry.write(
'\n "{}",'.format(paramType)
)
registry.write("\n },\n")
else:
registry.write("},\n")
registry.write(
' "{message[Resolution]}",\n'
" }}}},\n".format(message=message)
)
registry.write("\n};\n\nenum class Index\n{\n")
for index, (messageId, message) in enumerate(messages_sorted):
messageId = messageId[0].lower() + messageId[1:]
registry.write(" {} = {},\n".format(messageId, index))
registry.write(
"}};\n}} // namespace redfish::registries::{}\n".format(
namespace
)
)
def get_privilege_string_from_list(privilege_list):
privilege_string = "{{\n"
for privilege_json in privilege_list:
privileges = privilege_json["Privilege"]
privilege_string += " {"
for privilege in privileges:
if privilege == "NoAuth":
continue
privilege_string += '"'
privilege_string += privilege
privilege_string += '",\n'
if privilege != "NoAuth":
privilege_string = privilege_string[:-2]
privilege_string += "}"
privilege_string += ",\n"
privilege_string = privilege_string[:-2]
privilege_string += "\n}}"
return privilege_string
def get_variable_name_for_privilege_set(privilege_list):
names = []
for privilege_json in privilege_list:
privileges = privilege_json["Privilege"]
names.append("And".join(privileges))
return "Or".join(names)
PRIVILEGE_HEADER = (
PRAGMA_ONCE
+ WARNING
+ """
#include "privileges.hpp"
#include <array>
// clang-format off
namespace redfish::privileges
{
"""
)
def get_response_code(entry_id, entry):
codes = {
"InternalError": "internal_server_error",
"OperationTimeout": "internal_server_error",
"PropertyValueResourceConflict": "conflict",
"ResourceInUse": "service_unavailable",
"ServiceTemporarilyUnavailable": "service_unavailable",
"ResourceCannotBeDeleted": "method_not_allowed",
"PropertyValueModified": "ok",
"InsufficientPrivilege": "forbidden",
"AccountForSessionNoLongerExists": "forbidden",
"ServiceDisabled": "service_unavailable",
"ServiceInUnknownState": "service_unavailable",
"EventSubscriptionLimitExceeded": "service_unavailable",
"ResourceAtUriUnauthorized": "unauthorized",
"SessionTerminated": "ok",
"SubscriptionTerminated": "ok",
"PropertyNotWritable": "forbidden",
"MaximumErrorsExceeded": "internal_server_error",
"GeneralError": "internal_server_error",
"PreconditionFailed": "precondition_failed",
"OperationFailed": "bad_gateway",
"ServiceShuttingDown": "service_unavailable",
"AccountRemoved": "ok",
"PropertyValueExternalConflict": "conflict",
"InsufficientStorage": "insufficient_storage",
"OperationNotAllowed": "method_not_allowed",
"ResourceNotFound": "not_found",
"CouldNotEstablishConnection": "not_found",
"AccessDenied": "forbidden",
"Success": None,
"Created": "created",
"NoValidSession": "forbidden",
"SessionLimitExceeded": "service_unavailable",
"ResourceExhaustion": "service_unavailable",
"AccountModified": "ok",
"PasswordChangeRequired": None,
"ResourceInStandby": "service_unavailable",
"GenerateSecretKeyRequired": "forbidden",
}
code = codes.get(entry_id, "NOCODE")
if code != "NOCODE":
return code
return "bad_request"
def make_error_function(entry_id, entry, is_header):
arg_nonstring_types = {
"const boost::urls::url_view_base&": {
"AccessDenied": [1],
"CouldNotEstablishConnection": [1],
"GenerateSecretKeyRequired": [1],
"InvalidObject": [1],
"PasswordChangeRequired": [1],
"PropertyValueResourceConflict": [3],
"ResetRequired": [1],
"ResourceAtUriInUnknownFormat": [1],
"ResourceAtUriUnauthorized": [1],
"ResourceCreationConflict": [1],
"ResourceMissingAtURI": [1],
"SourceDoesNotSupportProtocol": [1],
},
"const nlohmann::json&": {
"ActionParameterValueError": [1],
"ActionParameterValueFormatError": [1],
"ActionParameterValueTypeError": [1],
"PropertyValueExternalConflict": [2],
"PropertyValueFormatError": [1],
"PropertyValueIncorrect": [2],
"PropertyValueModified": [2],
"PropertyValueNotInList": [1],
"PropertyValueOutOfRange": [1],
"PropertyValueResourceConflict": [2],
"PropertyValueTypeError": [1],
"QueryParameterValueFormatError": [1],
"QueryParameterValueTypeError": [1],
},
"uint64_t": {
"ArraySizeTooLong": [2],
"InvalidIndex": [1],
"StringValueTooLong": [2],
},
}
out = ""
args = []
argtypes = []
for arg_index, arg in enumerate(entry.get("ParamTypes", [])):
arg_index += 1
typename = "std::string_view"
for typestring, entries in arg_nonstring_types.items():
if arg_index in entries.get(entry_id, []):
typename = typestring
argtypes.append(typename)
args.append(f"{typename} arg{arg_index}")
function_name = entry_id[0].lower() + entry_id[1:]
arg = ", ".join(args)
out += f"nlohmann::json {function_name}({arg})"
if is_header:
out += ";\n\n"
else:
out += "\n{\n"
to_array_type = ""
if argtypes:
outargs = []
for index, typename in enumerate(argtypes):
index += 1
if typename == "const nlohmann::json&":
out += f"std::string arg{index}Str = arg{index}.dump(-1, ' ', true, nlohmann::json::error_handler_t::replace);\n"
elif typename == "uint64_t":
out += f"std::string arg{index}Str = std::to_string(arg{index});\n"
for index, typename in enumerate(argtypes):
index += 1
if typename == "const boost::urls::url_view_base&":
outargs.append(f"arg{index}.buffer()")
to_array_type = "<std::string_view>"
elif typename == "const nlohmann::json&":
outargs.append(f"arg{index}Str")
to_array_type = "<std::string_view>"
elif typename == "uint64_t":
outargs.append(f"arg{index}Str")
to_array_type = "<std::string_view>"
else:
outargs.append(f"arg{index}")
argstring = ", ".join(outargs)
if argtypes:
arg_param = f"std::to_array{to_array_type}({{{argstring}}})"
else:
arg_param = "{}"
out += f" return getLog(redfish::registries::base::Index::{function_name}, {arg_param});"
out += "\n}\n\n"
args.insert(0, "crow::Response& res")
if entry_id == "InternalError":
if is_header:
args.append(
"std::source_location location = std::source_location::current()"
)
else:
args.append("const std::source_location location")
arg = ", ".join(args)
out += f"void {function_name}({arg})"
if is_header:
out += ";\n"
else:
out += "\n{\n"
if entry_id == "InternalError":
out += """BMCWEB_LOG_CRITICAL("Internal Error {}({}:{}) `{}`: ", location.file_name(),
location.line(), location.column(),
location.function_name());\n"""
if entry_id == "ServiceTemporarilyUnavailable":
out += (
"res.addHeader(boost::beast::http::field::retry_after, arg1);"
)
res = get_response_code(entry_id, entry)
if res:
out += f" res.result(boost::beast::http::status::{res});\n"
args_out = ", ".join([f"arg{x+1}" for x in range(len(argtypes))])
addMessageToJson = {
"PropertyDuplicate": 1,
"ResourceAlreadyExists": 2,
"CreateFailedMissingReqProperties": 1,
"PropertyValueFormatError": 2,
"PropertyValueNotInList": 2,
"PropertyValueTypeError": 2,
"PropertyValueError": 1,
"PropertyNotWritable": 1,
"PropertyValueModified": 1,
"PropertyMissing": 1,
}
addMessageToRoot = [
"SessionTerminated",
"SubscriptionTerminated",
"AccountRemoved",
"Created",
"Success",
"PasswordChangeRequired",
]
if entry_id in addMessageToJson:
out += f" addMessageToJson(res.jsonValue, {function_name}({args_out}), arg{addMessageToJson[entry_id]});\n"
elif entry_id in addMessageToRoot:
out += f" addMessageToJsonRoot(res.jsonValue, {function_name}({args_out}));\n"
else:
out += f" addMessageToErrorJson(res.jsonValue, {function_name}({args_out}));\n"
out += "}\n"
out += "\n"
return out
def create_error_registry(entry, registry_version):
file, json_dict, namespace, url = entry
error_messages_hpp = os.path.join(
SCRIPT_DIR, "..", "redfish-core", "include", "error_messages.hpp"
)
messages = json_dict["Messages"]
with open(
error_messages_hpp,
"w",
) as out:
out.write(PRAGMA_ONCE)
out.write(WARNING)
out.write(
"""
#include "http_response.hpp"
#include <boost/url/url_view_base.hpp>
#include <nlohmann/json.hpp>
#include <cstdint>
#include <source_location>
#include <string>
#include <string_view>
// IWYU pragma: no_forward_declare crow::Response
namespace redfish
{
namespace messages
{
"""
)
out.write(
f'constexpr const char* messageVersionPrefix = "Base.{registry_version}.";'
)
out.write(
"""
constexpr const char* messageAnnotation = "@Message.ExtendedInfo";
/**
* @brief Moves all error messages from the |source| JSON to |target|
*/
void moveErrorsToErrorJson(nlohmann::json& target, nlohmann::json& source);
"""
)
for entry_id, entry in messages.items():
message = entry["Message"]
for index in range(1, 10):
message = message.replace(f"'%{index}'", f"<arg{index}>")
message = message.replace(f"%{index}", f"<arg{index}>")
out.write("/**\n")
out.write(f"* @brief Formats {entry_id} message into JSON\n")
out.write(f'* Message body: "{message}"\n')
out.write("*\n")
arg_index = 0
for arg_index, arg in enumerate(entry.get("ParamTypes", [])):
arg_index += 1
out.write(
f"* @param[in] arg{arg_index} Parameter of message that will replace %{arg_index} in its body.\n"
)
out.write("*\n")
out.write(f"* @returns Message {entry_id} formatted to JSON */\n")
out.write(make_error_function(entry_id, entry, True))
out.write(" }\n")
out.write("}\n")
error_messages_cpp = os.path.join(
SCRIPT_DIR, "..", "redfish-core", "src", "error_messages.cpp"
)
with open(
error_messages_cpp,
"w",
) as out:
out.write(WARNING)
out.write(
"""
#include "error_messages.hpp"
#include "http_response.hpp"
#include "logging.hpp"
#include "registries.hpp"
#include "registries/base_message_registry.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/url/url_view_base.hpp>
#include <nlohmann/json.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <source_location>
#include <span>
#include <string>
#include <string_view>
// Clang can't seem to decide whether this header needs to be included or not,
// and is inconsistent. Include it for now
// NOLINTNEXTLINE(misc-include-cleaner)
#include <utility>
namespace redfish
{
namespace messages
{
static void addMessageToErrorJson(nlohmann::json& target,
const nlohmann::json& message)
{
auto& error = target["error"];
// If this is the first error message, fill in the information from the
// first error message to the top level struct
if (!error.is_object())
{
auto messageIdIterator = message.find("MessageId");
if (messageIdIterator == message.end())
{
BMCWEB_LOG_CRITICAL(
"Attempt to add error message without MessageId");
return;
}
auto messageFieldIterator = message.find("Message");
if (messageFieldIterator == message.end())
{
BMCWEB_LOG_CRITICAL("Attempt to add error message without Message");
return;
}
error["code"] = *messageIdIterator;
error["message"] = *messageFieldIterator;
}
else
{
// More than 1 error occurred, so the message has to be generic
error["code"] = std::string(messageVersionPrefix) + "GeneralError";
error["message"] = "A general error has occurred. See Resolution for "
"information on how to resolve the error.";
}
// This check could technically be done in the default construction
// branch above, but because we need the pointer to the extended info field
// anyway, it's more efficient to do it here.
auto& extendedInfo = error[messages::messageAnnotation];
if (!extendedInfo.is_array())
{
extendedInfo = nlohmann::json::array();
}
extendedInfo.push_back(message);
}
void moveErrorsToErrorJson(nlohmann::json& target, nlohmann::json& source)
{
if (!source.is_object())
{
return;
}
auto errorIt = source.find("error");
if (errorIt == source.end())
{
// caller puts error message in root
messages::addMessageToErrorJson(target, source);
source.clear();
return;
}
auto extendedInfoIt = errorIt->find(messages::messageAnnotation);
if (extendedInfoIt == errorIt->end())
{
return;
}
const nlohmann::json::array_t* extendedInfo =
(*extendedInfoIt).get_ptr<const nlohmann::json::array_t*>();
if (extendedInfo == nullptr)
{
source.erase(errorIt);
return;
}
for (const nlohmann::json& message : *extendedInfo)
{
addMessageToErrorJson(target, message);
}
source.erase(errorIt);
}
static void addMessageToJsonRoot(nlohmann::json& target,
const nlohmann::json& message)
{
if (!target[messages::messageAnnotation].is_array())
{
// Force object to be an array
target[messages::messageAnnotation] = nlohmann::json::array();
}
target[messages::messageAnnotation].push_back(message);
}
static void addMessageToJson(nlohmann::json& target,
const nlohmann::json& message,
std::string_view fieldPath)
{
std::string extendedInfo(fieldPath);
extendedInfo += messages::messageAnnotation;
nlohmann::json& field = target[extendedInfo];
if (!field.is_array())
{
// Force object to be an array
field = nlohmann::json::array();
}
// Object exists and it is an array so we can just push in the message
field.push_back(message);
}
static nlohmann::json getLog(redfish::registries::base::Index name,
std::span<const std::string_view> args)
{
size_t index = static_cast<size_t>(name);
if (index >= redfish::registries::base::registry.size())
{
return {};
}
return getLogFromRegistry(redfish::registries::base::header,
redfish::registries::base::registry, index, args);
}
"""
)
for entry_id, entry in messages.items():
out.write(
f"""/**
* @internal
* @brief Formats {entry_id} message into JSON
*
* See header file for more information
* @endinternal
*/
"""
)
message = entry["Message"]
out.write(make_error_function(entry_id, entry, False))
out.write(" }\n")
out.write("}\n")
os.system(f"clang-format -i {error_messages_hpp} {error_messages_cpp}")
def make_privilege_registry():
path, json_file, type_name, url = make_getter(
"Redfish_1.5.0_PrivilegeRegistry.json",
"privilege_registry.hpp",
"privilege",
)
with open(path, "w") as registry:
registry.write(PRIVILEGE_HEADER)
privilege_dict = {}
for mapping in json_file["Mappings"]:
# first pass, identify all the unique privilege sets
for operation, privilege_list in mapping["OperationMap"].items():
privilege_dict[
get_privilege_string_from_list(privilege_list)
] = (privilege_list,)
for index, key in enumerate(privilege_dict):
(privilege_list,) = privilege_dict[key]
name = get_variable_name_for_privilege_set(privilege_list)
registry.write(
"const std::array<Privileges, {length}> "
"privilegeSet{name} = {key};\n".format(
length=len(privilege_list), name=name, key=key
)
)
privilege_dict[key] = (privilege_list, name)
for mapping in json_file["Mappings"]:
entity = mapping["Entity"]
registry.write("// {}\n".format(entity))
for operation, privilege_list in mapping["OperationMap"].items():
privilege_string = get_privilege_string_from_list(
privilege_list
)
operation = operation.lower()
registry.write(
"const static auto& {}{} = privilegeSet{};\n".format(
operation, entity, privilege_dict[privilege_string][1]
)
)
registry.write("\n")
registry.write(
"} // namespace redfish::privileges\n// clang-format on\n"
)
def to_pascal_case(text):
s = text.replace("_", " ")
s = s.split()
if len(text) == 0:
return text
return "".join(i.capitalize() for i in s[0:])
def main():
dmtf_registries = (
("base", "1.19.0"),
("composition", "1.1.2"),
("environmental", "1.0.1"),
("ethernet_fabric", "1.0.1"),
("fabric", "1.0.2"),
("heartbeat_event", "1.0.1"),
("job_event", "1.0.1"),
("license", "1.0.3"),
("log_service", "1.0.1"),
("network_device", "1.0.3"),
("platform", "1.0.1"),
("power", "1.0.1"),
("resource_event", "1.3.0"),
("sensor_event", "1.0.1"),
("storage_device", "1.2.1"),
("task_event", "1.0.3"),
("telemetry", "1.0.0"),
("update", "1.0.2"),
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--registries",
type=str,
default="privilege,openbmc,"
+ ",".join([dmtf[0] for dmtf in dmtf_registries]),
help="Comma delimited list of registries to update",
)
args = parser.parse_args()
registries = set(args.registries.split(","))
files = []
for registry, version in dmtf_registries:
if registry in registries:
registry_pascal_case = to_pascal_case(registry)
files.append(
make_getter(
f"{registry_pascal_case}.{version}.json",
f"{registry}_message_registry.hpp",
registry,
)
)
if "openbmc" in registries:
files.append(openbmc_local_getter())
update_registries(files)
create_error_registry(files[0], dmtf_registries[0][1])
if "privilege" in registries:
make_privilege_registry()
if __name__ == "__main__":
main()