1#!/usr/bin/env python3 2 3# Script to generate top level resource collection URIs 4# Parses the Redfish schema to determine what are the top level collection URIs 5# and writes them to a generated .hpp file as an unordered_set. Also generates 6# a map of URIs that contain a top level collection as part of their subtree. 7# Those URIs as well as those of their immediate children as written as an 8# unordered_map. These URIs are need by Redfish Aggregation 9 10import os 11import xml.etree.ElementTree as ET 12 13WARNING = """/**************************************************************** 14 * READ THIS WARNING FIRST 15 * This is an auto-generated header which contains definitions 16 * for Redfish DMTF defined schemas. 17 * DO NOT modify this registry outside of running the 18 * update_schemas.py script. The definitions contained within 19 * this file are owned by DMTF. Any modifications to these files 20 * should be first pushed to the relevant registry in the DMTF 21 * github organization. 22 ***************************************************************/""" 23 24SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) 25REDFISH_SCHEMA_DIR = os.path.realpath( 26 os.path.join(SCRIPT_DIR, "..", "static", "redfish", "v1", "schema") 27) 28CPP_OUTFILE = os.path.realpath( 29 os.path.join( 30 SCRIPT_DIR, "..", "redfish-core", "include", "aggregation_utils.hpp" 31 ) 32) 33 34 35# Odata string types 36EDMX = "{http://docs.oasis-open.org/odata/ns/edmx}" 37EDM = "{http://docs.oasis-open.org/odata/ns/edm}" 38 39seen_paths = set() 40 41 42def parse_node(target_entitytype, path, top_collections, found_top, xml_file): 43 filepath = os.path.join(REDFISH_SCHEMA_DIR, xml_file) 44 tree = ET.parse(filepath) 45 root = tree.getroot() 46 47 # Map xml URIs to their associated namespace 48 xml_map = {} 49 for ref in root.findall(EDMX + "Reference"): 50 uri = ref.get("Uri") 51 if uri is None: 52 continue 53 file = uri.split("/").pop() 54 for inc in ref.findall(EDMX + "Include"): 55 namespace = inc.get("Namespace") 56 if namespace is None: 57 continue 58 xml_map[namespace] = file 59 60 parse_root( 61 root, target_entitytype, path, top_collections, found_top, xml_map 62 ) 63 64 65# Given a root node we want to parse the tree to find all instances of a 66# specific EntityType. This is a separate routine so that we can rewalk the 67# current tree when a NavigationProperty Type links to the current file. 68def parse_root( 69 root, target_entitytype, path, top_collections, found_top, xml_map 70): 71 ds = root.find(EDMX + "DataServices") 72 for schema in ds.findall(EDM + "Schema"): 73 for entity_type in schema.findall(EDM + "EntityType"): 74 name = entity_type.get("Name") 75 if name != target_entitytype: 76 continue 77 for nav_prop in entity_type.findall(EDM + "NavigationProperty"): 78 parse_navigation_property( 79 root, 80 name, 81 nav_prop, 82 path, 83 top_collections, 84 found_top, 85 xml_map, 86 ) 87 88 # These ComplexType objects contain links to actual resources or 89 # resource collections 90 for complex_type in schema.findall(EDM + "ComplexType"): 91 name = complex_type.get("Name") 92 if name != target_entitytype: 93 continue 94 for nav_prop in complex_type.findall(EDM + "NavigationProperty"): 95 parse_navigation_property( 96 root, 97 name, 98 nav_prop, 99 path, 100 top_collections, 101 found_top, 102 xml_map, 103 ) 104 105 106# Helper function which expects a NavigationProperty to be passed in. We need 107# this because NavigationProperty appears under both EntityType and ComplexType 108def parse_navigation_property( 109 root, curr_entitytype, element, path, top_collections, found_top, xml_map 110): 111 if element.tag != (EDM + "NavigationProperty"): 112 return 113 114 # We don't want to actually parse this property if it's just an excerpt 115 for annotation in element.findall(EDM + "Annotation"): 116 term = annotation.get("Term") 117 if term == "Redfish.ExcerptCopy": 118 return 119 120 # We don't want to aggregate JsonSchemas as well as anything under 121 # AccountService or SessionService 122 nav_name = element.get("Name") 123 if nav_name in ["JsonSchemas", "AccountService", "SessionService"]: 124 return 125 126 nav_type = element.get("Type") 127 if "Collection" in nav_type: 128 # Type is either Collection(<Namespace>.<TypeName>) or 129 # Collection(<NamespaceName>.<NamespaceVersion>.<TypeName>) 130 if nav_type.startswith("Collection"): 131 qualified_name = nav_type.split("(")[1].split(")")[0] 132 # Do we need to parse this file or another file? 133 qualified_name_split = qualified_name.split(".") 134 if len(qualified_name_split) == 3: 135 typename = qualified_name_split[2] 136 else: 137 typename = qualified_name_split[1] 138 file_key = qualified_name_split[0] 139 140 # If we contain a collection array then we don't want to add the 141 # name to the path if we're a collection schema 142 if nav_name != "Members": 143 path += "/" + nav_name 144 if path in seen_paths: 145 return 146 seen_paths.add(path) 147 148 # Did we find the top level collection in the current path or 149 # did we previously find it? 150 if not found_top: 151 top_collections.add(path) 152 found_top = True 153 154 member_id = typename + "Id" 155 prev_count = path.count(member_id) 156 if prev_count: 157 new_path = path + "/{" + member_id + str(prev_count + 1) + "}" 158 else: 159 new_path = path + "/{" + member_id + "}" 160 161 # type is "<Namespace>.<TypeName>", both should end with "Collection" 162 else: 163 # Escape if we've found a circular dependency like SubProcessors 164 if path.count(nav_name) >= 2: 165 return 166 167 nav_type_split = nav_type.split(".") 168 if (len(nav_type_split) != 2) and ( 169 nav_type_split[0] != nav_type_split[1] 170 ): 171 # We ended up with something like Resource.ResourceCollection 172 return 173 file_key = nav_type_split[0] 174 typename = nav_type_split[1] 175 new_path = path + "/" + nav_name 176 177 # Did we find the top level collection in the current path or did we 178 # previously find it? 179 if not found_top: 180 top_collections.add(new_path) 181 found_top = True 182 183 # NavigationProperty is not for a collection 184 else: 185 # Bail if we've found a circular dependency like MetricReport 186 if path.count(nav_name): 187 return 188 189 new_path = path + "/" + nav_name 190 nav_type_split = nav_type.split(".") 191 file_key = nav_type_split[0] 192 typename = nav_type_split[1] 193 194 # We need to specially handle certain URIs since the Name attribute from the 195 # schema is not used as part of the path 196 # TODO: Expand this section to add special handling across the entirety of 197 # the Redfish tree 198 new_path2 = "" 199 if new_path == "/redfish/v1/Tasks": 200 new_path2 = "/redfish/v1/TaskService" 201 202 # If we had to apply special handling then we need to remove the inital 203 # version of the URI if it was previously added 204 if new_path2 != "": 205 if new_path in seen_paths: 206 seen_paths.remove(new_path) 207 new_path = new_path2 208 209 # No need to parse the new URI if we've already done so 210 if new_path in seen_paths: 211 return 212 seen_paths.add(new_path) 213 214 # We can stop parsing if we've found a top level collection 215 # TODO: Don't return here when we want to walk the entire tree instead 216 if found_top: 217 return 218 219 # If the namespace of the NavigationProperty's Type is not in our xml map 220 # then that means it inherits from elsewhere in the current file 221 if file_key in xml_map: 222 parse_node( 223 typename, new_path, top_collections, found_top, xml_map[file_key] 224 ) 225 else: 226 parse_root( 227 root, typename, new_path, top_collections, found_top, xml_map 228 ) 229 230 231def generate_top_collections(): 232 # We need to separately track top level resources as well as all URIs that 233 # are upstream from a top level resource. We shouldn't combine these into 234 # a single structure because: 235 # 236 # 1) We want direct lookup of top level collections for prefix handling 237 # purposes. 238 # 239 # 2) A top level collection will not always be one level below the service 240 # root. For example, we need to aggregate 241 # /redfish/v1/CompositionService/ActivePool and we do not currently support 242 # CompositionService. If a satellite BMC implements it then we would need 243 # to display a link to CompositionService under /redfish/v1 even though 244 # CompositionService is not a top level collection. 245 246 # Contains URIs for all top level collections 247 top_collections = set() 248 249 # Begin parsing from the Service Root 250 curr_path = "/redfish/v1" 251 seen_paths.add(curr_path) 252 parse_node( 253 "ServiceRoot", curr_path, top_collections, False, "ServiceRoot_v1.xml" 254 ) 255 256 print("Finished traversal!") 257 258 TOTAL = len(top_collections) 259 with open(CPP_OUTFILE, "w") as hpp_file: 260 hpp_file.write( 261 "#pragma once\n" 262 "{WARNING}\n" 263 "// clang-format off\n" 264 "#include <array>\n" 265 "#include <string_view>\n" 266 "\n" 267 "namespace redfish\n" 268 "{{\n" 269 '// Note that each URI actually begins with "/redfish/v1"\n' 270 "// They've been omitted to save space and reduce search time\n" 271 "constexpr std::array<std::string_view, {TOTAL}> " 272 "topCollections{{\n".format(WARNING=WARNING, TOTAL=TOTAL) 273 ) 274 275 for collection in sorted(top_collections): 276 # All URIs start with "/redfish/v1". We can omit that portion to 277 # save memory and reduce lookup time 278 hpp_file.write( 279 ' "{}",\n'.format(collection.split("/redfish/v1")[1]) 280 ) 281 282 hpp_file.write("};\n} // namespace redfish\n") 283