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