xref: /openbmc/bmcweb/scripts/generate_schema_collections.py (revision 40e9b92ec19acffb46f83a6e55b18974da5d708e)
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