blob: ca6844fada05fbb942b91b5b2fb20dffb030e3d1 [file] [log] [blame]
Carson Labrado9e031402022-07-08 20:56:52 +00001#!/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
Ed Tanous40e9b922024-09-10 13:50:16 -070013WARNING = """
14/****************************************************************
Carson Labrado9e031402022-07-08 20:56:52 +000015 * 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__))
Carson Labrado9e031402022-07-08 20:56:52 +000026CPP_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
Ed Tanous720c9892024-05-11 07:28:09 -070040def 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
Carson Labrado9e031402022-07-08 20:56:52 +000050def parse_node(target_entitytype, path, top_collections, found_top, xml_file):
Ed Tanous720c9892024-05-11 07:28:09 -070051
52 filepath = resolve_filename(xml_file)
Carson Labrado9e031402022-07-08 20:56:52 +000053 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
Ed Tanous8ece0e42024-01-02 13:16:50 -0800211 # If we had to apply special handling then we need to remove the initial
Carson Labrado9e031402022-07-08 20:56:52 +0000212 # 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
Ed Tanousfdbce792024-06-26 14:48:46 -0700265 # 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
Carson Labrado9e031402022-07-08 20:56:52 +0000271 print("Finished traversal!")
272
273 TOTAL = len(top_collections)
274 with open(CPP_OUTFILE, "w") as hpp_file:
275 hpp_file.write(
Ed Tanous40e9b922024-09-10 13:50:16 -0700276 "// SPDX-License-Identifier: Apache-2.0\n"
277 "// SPDX-FileCopyrightText: Copyright OpenBMC Authors\n"
Carson Labrado9e031402022-07-08 20:56:52 +0000278 "#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"
Carson Labrado11987af2022-11-23 20:55:18 +0000286 '// Note that each URI actually begins with "/redfish/v1"\n'
Carson Labrado9e031402022-07-08 20:56:52 +0000287 "// 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):
Carson Labrado11987af2022-11-23 20:55:18 +0000293 # All URIs start with "/redfish/v1". We can omit that portion to
Carson Labrado9e031402022-07-08 20:56:52 +0000294 # save memory and reduce lookup time
295 hpp_file.write(
Carson Labrado11987af2022-11-23 20:55:18 +0000296 ' "{}",\n'.format(collection.split("/redfish/v1")[1])
Carson Labrado9e031402022-07-08 20:56:52 +0000297 )
298
299 hpp_file.write("};\n} // namespace redfish\n")