#!/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__)) 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 resolve_filename(xml_file): for root, dirs, files in os.walk( os.path.join(SCRIPT_DIR, "..", "redfish-core", "schema") ): for csdl_file in files: if csdl_file == xml_file: return os.path.join(root, csdl_file) raise Exception(f"Could not resolve {xml_file} in search folders") def parse_node(target_entitytype, path, top_collections, found_top, xml_file): filepath = resolve_filename(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(.) or # Collection(..) 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 ".", 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 initial # 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" ) # Task service is not called out by CSDL, and is technically not a # collection, but functionally needs to be treated like a collection, per # the Asynchronous operations section of DSP0266 # https://www.dmtf.org/sites/default/files/standards/documents/DSP0266_1.20.1.html#asynchronous-operations top_collections.add("/redfish/v1/TaskService/TaskMonitors") 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 \n" "#include \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 " "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")