| #!/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 | 
 |  | 
 | 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): | 
 |     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 | 
 | ): | 
 |     ds = root.find(EDMX + "DataServices") | 
 |     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 | 
 | ): | 
 |     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 | 
 |     # 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 != "": | 
 |         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 | 
 |     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(): | 
 |     # 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) | 
 |     with open(CPP_OUTFILE, "w") as hpp_file: | 
 |         hpp_file.write( | 
 |             "#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") |