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