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__))
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
202    # If we had to apply special handling then we need to remove the initial
203    # 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"
269            '// Note that each URI actually begins with "/redfish/v1"\n'
270            "// 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):
276            # All URIs start with "/redfish/v1".  We can omit that portion to
277            # save memory and reduce lookup time
278            hpp_file.write(
279                '    "{}",\n'.format(collection.split("/redfish/v1")[1])
280            )
281
282        hpp_file.write("};\n} // namespace redfish\n")
283