blob: c44ca5915509e0044f0aee85681325ef97e17fcf [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
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
Ed Tanous8ece0e42024-01-02 13:16:50 -0800202 # If we had to apply special handling then we need to remove the initial
Carson Labrado9e031402022-07-08 20:56:52 +0000203 # 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"
Carson Labrado11987af2022-11-23 20:55:18 +0000269 '// Note that each URI actually begins with "/redfish/v1"\n'
Carson Labrado9e031402022-07-08 20:56:52 +0000270 "// 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):
Carson Labrado11987af2022-11-23 20:55:18 +0000276 # All URIs start with "/redfish/v1". We can omit that portion to
Carson Labrado9e031402022-07-08 20:56:52 +0000277 # save memory and reduce lookup time
278 hpp_file.write(
Carson Labrado11987af2022-11-23 20:55:18 +0000279 ' "{}",\n'.format(collection.split("/redfish/v1")[1])
Carson Labrado9e031402022-07-08 20:56:52 +0000280 )
281
282 hpp_file.write("};\n} // namespace redfish\n")