| #!/usr/bin/env python3 | |
| """Script to generate top level resource collection URIs. | |
| Parses the Redfish schema to determine what are the top level collection URIs | |
| and writes them to a generated .hpp file as an unordered_set. Also generates | |
| a map of URIs that contain a top level collection as part of their subtree. | |
| Those URIs as well as those of their immediate children as written as an | |
| unordered_map. These URIs are need by Redfish Aggregation | |
| generates redfish-core/include/aggregation_utils.hpp | |
| """ | |
| import os | |
| import xml.etree.ElementTree as ET | |
| WARNING = """/**************************************************************** | |
| * READ THIS WARNING FIRST | |
| * This is an auto-generated header which contains definitions | |
| * for Redfish DMTF defined schemas. | |
| * DO NOT modify this registry outside of running the | |
| * update_schemas.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. | |
| ***************************************************************/""" | |
| SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) | |
| REDFISH_SCHEMA_DIR = os.path.realpath( | |
| os.path.join(SCRIPT_DIR, "..", "static", "redfish", "v1", "schema") | |
| ) | |
| CPP_OUTFILE = os.path.realpath( | |
| os.path.join( | |
| SCRIPT_DIR, "..", "redfish-core", "include", "aggregation_utils.hpp" | |
| ) | |
| ) | |
| # Odata string types | |
| EDMX = "{http://docs.oasis-open.org/odata/ns/edmx}" | |
| EDM = "{http://docs.oasis-open.org/odata/ns/edm}" | |
| seen_paths = set() | |
| def parse_node(target_entitytype, path, top_collections, found_top, xml_file): | |
| """Parses a single node in the Redfish schema.""" | |
| filepath = os.path.join(REDFISH_SCHEMA_DIR, xml_file) | |
| tree = ET.parse(filepath) | |
| root = tree.getroot() | |
| # Map xml URIs to their associated namespace | |
| xml_map = {} | |
| for ref in root.findall(EDMX + "Reference"): | |
| uri = ref.get("Uri") | |
| if uri is None: | |
| continue | |
| file = uri.split("/").pop() | |
| for inc in ref.findall(EDMX + "Include"): | |
| namespace = inc.get("Namespace") | |
| if namespace is None: | |
| continue | |
| xml_map[namespace] = file | |
| parse_root(root, target_entitytype, path, top_collections, found_top, xml_map) | |
| # Given a root node we want to parse the tree to find all instances of a | |
| # specific EntityType. This is a separate routine so that we can rewalk the | |
| # current tree when a NavigationProperty Type links to the current file. | |
| def parse_root( | |
| root, target_entitytype, path, top_collections, found_top, xml_map | |
| ): | |
| """Parses a single root node in the Redfish schema.""" | |
| ds = root.find(EDMX + "DataServices") | |
| if ds is None: | |
| return | |
| for schema in ds.findall(EDM + "Schema"): | |
| for entity_type in schema.findall(EDM + "EntityType"): | |
| name = entity_type.get("Name") | |
| if name != target_entitytype: | |
| continue | |
| for nav_prop in entity_type.findall(EDM + "NavigationProperty"): | |
| parse_navigation_property( | |
| root, | |
| name, | |
| nav_prop, | |
| path, | |
| top_collections, | |
| found_top, | |
| xml_map, | |
| ) | |
| # These ComplexType objects contain links to actual resources or | |
| # resource collections | |
| for complex_type in schema.findall(EDM + "ComplexType"): | |
| name = complex_type.get("Name") | |
| if name != target_entitytype: | |
| continue | |
| for nav_prop in complex_type.findall(EDM + "NavigationProperty"): | |
| parse_navigation_property( | |
| root, | |
| name, | |
| nav_prop, | |
| path, | |
| top_collections, | |
| found_top, | |
| xml_map, | |
| ) | |
| # Helper function which expects a NavigationProperty to be passed in. We need | |
| # this because NavigationProperty appears under both EntityType and ComplexType | |
| def parse_navigation_property( | |
| root, curr_entitytype, element, path, top_collections, found_top, xml_map # pylint: disable=unused-argument | |
| ): | |
| """Parses a single NavigationProperty.""" | |
| if element.tag != (EDM + "NavigationProperty"): | |
| return | |
| # We don't want to actually parse this property if it's just an excerpt | |
| for annotation in element.findall(EDM + "Annotation"): | |
| term = annotation.get("Term") | |
| if term == "Redfish.ExcerptCopy": | |
| return | |
| # We don't want to aggregate JsonSchemas as well as anything under | |
| # AccountService or SessionService | |
| nav_name = element.get("Name") | |
| if nav_name in ["JsonSchemas", "AccountService", "SessionService"]: | |
| return | |
| nav_type = element.get("Type") | |
| if "Collection" in nav_type: | |
| # Type is either Collection(<Namespace>.<TypeName>) or | |
| # Collection(<NamespaceName>.<NamespaceVersion>.<TypeName>) | |
| if nav_type.startswith("Collection"): | |
| qualified_name = nav_type.split("(")[1].split(")")[0] | |
| # Do we need to parse this file or another file? | |
| qualified_name_split = qualified_name.split(".") | |
| if len(qualified_name_split) == 3: | |
| typename = qualified_name_split[2] | |
| else: | |
| typename = qualified_name_split[1] | |
| file_key = qualified_name_split[0] | |
| # If we contain a collection array then we don't want to add the | |
| # name to the path if we're a collection schema | |
| if nav_name != "Members": | |
| path += "/" + nav_name | |
| if path in seen_paths: | |
| return | |
| seen_paths.add(path) | |
| # Did we find the top level collection in the current path or | |
| # did we previously find it? | |
| if not found_top: | |
| top_collections.add(path) | |
| found_top = True | |
| member_id = typename + "Id" | |
| prev_count = path.count(member_id) | |
| if prev_count: | |
| new_path = path + "/{" + member_id + str(prev_count + 1) + "}" | |
| else: | |
| new_path = path + "/{" + member_id + "}" | |
| # type is "<Namespace>.<TypeName>", both should end with "Collection" | |
| else: | |
| # Escape if we've found a circular dependency like SubProcessors | |
| if path.count(nav_name) >= 2: | |
| return | |
| nav_type_split = nav_type.split(".") | |
| if (len(nav_type_split) != 2) and ( | |
| nav_type_split[0] != nav_type_split[1] | |
| ): | |
| # We ended up with something like Resource.ResourceCollection | |
| return | |
| file_key = nav_type_split[0] | |
| typename = nav_type_split[1] | |
| new_path = path + "/" + nav_name | |
| # Did we find the top level collection in the current path or did we | |
| # previously find it? | |
| if not found_top: | |
| top_collections.add(new_path) | |
| found_top = True | |
| # NavigationProperty is not for a collection | |
| else: | |
| # Bail if we've found a circular dependency like MetricReport | |
| if path.count(nav_name): | |
| return | |
| new_path = path + "/" + nav_name | |
| nav_type_split = nav_type.split(".") | |
| file_key = nav_type_split[0] | |
| typename = nav_type_split[1] | |
| # We need to specially handle certain URIs since the Name attribute from the | |
| # schema is not used as part of the path | |
| # TODO: Expand this section to add special handling across the entirety of # pylint: disable=g-bad-todo | |
| # the Redfish tree | |
| new_path2 = "" | |
| if new_path == "/redfish/v1/Tasks": | |
| new_path2 = "/redfish/v1/TaskService" | |
| # If we had to apply special handling then we need to remove the inital | |
| # version of the URI if it was previously added | |
| if new_path2 != "": # pylint: disable=g-explicit-bool-comparison | |
| if new_path in seen_paths: | |
| seen_paths.remove(new_path) | |
| new_path = new_path2 | |
| # No need to parse the new URI if we've already done so | |
| if new_path in seen_paths: | |
| return | |
| seen_paths.add(new_path) | |
| # We can stop parsing if we've found a top level collection | |
| # TODO: Don't return here when we want to walk the entire tree instead # pylint: disable=g-bad-todo | |
| if found_top: | |
| return | |
| # If the namespace of the NavigationProperty's Type is not in our xml map | |
| # then that means it inherits from elsewhere in the current file | |
| if file_key in xml_map: | |
| parse_node( | |
| typename, new_path, top_collections, found_top, xml_map[file_key] | |
| ) | |
| else: | |
| parse_root(root, typename, new_path, top_collections, found_top, xml_map) | |
| def generate_top_collections(): | |
| """Generates the top level collections and writes them to a file.""" | |
| # We need to separately track top level resources as well as all URIs that | |
| # are upstream from a top level resource. We shouldn't combine these into | |
| # a single structure because: | |
| # | |
| # 1) We want direct lookup of top level collections for prefix handling | |
| # purposes. | |
| # | |
| # 2) A top level collection will not always be one level below the service | |
| # root. For example, we need to aggregate | |
| # /redfish/v1/CompositionService/ActivePool and we do not currently support | |
| # CompositionService. If a satellite BMC implements it then we would need | |
| # to display a link to CompositionService under /redfish/v1 even though | |
| # CompositionService is not a top level collection. | |
| # Contains URIs for all top level collections | |
| top_collections = set() | |
| # Begin parsing from the Service Root | |
| curr_path = "/redfish/v1" | |
| seen_paths.add(curr_path) | |
| parse_node( | |
| "ServiceRoot", curr_path, top_collections, False, "ServiceRoot_v1.xml" | |
| ) | |
| print("Finished traversal!") | |
| TOTAL = len(top_collections) # pylint: disable=invalid-name | |
| with open(CPP_OUTFILE, "w") as hpp_file: | |
| hpp_file.write( | |
| "// NOLINTBEGIN\n" | |
| "#pragma once\n" | |
| "{WARNING}\n" | |
| "// clang-format off\n" | |
| "#include <array>\n" | |
| "#include <string_view>\n" | |
| "\n" | |
| "namespace redfish\n" | |
| "{{\n" | |
| '// Note that each URI actually begins with "/redfish/v1"\n' | |
| "// They've been omitted to save space and reduce search time\n" | |
| "constexpr std::array<std::string_view, {TOTAL}> " | |
| "topCollections{{\n".format(WARNING=WARNING, TOTAL=TOTAL) | |
| ) | |
| for collection in sorted(top_collections): | |
| # All URIs start with "/redfish/v1". We can omit that portion to | |
| # save memory and reduce lookup time | |
| hpp_file.write(' "{}",\n'.format(collection.split("/redfish/v1")[1])) | |
| hpp_file.write("};\n} // namespace redfish\n// NOLINTEND\n") |