xref: /openbmc/bmcweb/scripts/update_schemas.py (revision a7405d5fe77fa9e324c22cb29ea08029fc023f4e)
1#!/usr/bin/env python3
2import os
3import shutil
4import xml.etree.ElementTree as ET
5import zipfile
6from collections import OrderedDict, defaultdict
7from io import BytesIO
8
9import generate_schema_enums
10import requests
11from generate_schema_collections import generate_top_collections
12
13VERSION = "DSP8010_2022.3"
14
15WARNING = """/****************************************************************
16 *                 READ THIS WARNING FIRST
17 * This is an auto-generated header which contains definitions
18 * for Redfish DMTF defined schemas.
19 * DO NOT modify this registry outside of running the
20 * update_schemas.py script.  The definitions contained within
21 * this file are owned by DMTF.  Any modifications to these files
22 * should be first pushed to the relevant registry in the DMTF
23 * github organization.
24 ***************************************************************/"""
25
26# To use a new schema, add to list and rerun tool
27include_list = [
28    "AccountService",
29    "ActionInfo",
30    "AggregationService",
31    "Assembly",
32    "AttributeRegistry",
33    "Bios",
34    "Cable",
35    "CableCollection",
36    "Certificate",
37    "CertificateCollection",
38    "CertificateLocations",
39    "CertificateService",
40    "Chassis",
41    "ChassisCollection",
42    "ComputerSystem",
43    "ComputerSystemCollection",
44    "Drive",
45    "DriveCollection",
46    "EnvironmentMetrics",
47    "EthernetInterface",
48    "EthernetInterfaceCollection",
49    "Event",
50    "EventDestination",
51    "EventDestinationCollection",
52    "EventService",
53    "FabricAdapter",
54    "FabricAdapterCollection",
55    "Fan",
56    "FanCollection",
57    "IPAddresses",
58    "JsonSchemaFile",
59    "JsonSchemaFileCollection",  # redfish/v1/JsonSchemas
60    "LogEntry",
61    "LogEntryCollection",
62    "LogService",
63    "LogServiceCollection",
64    "Manager",
65    "ManagerAccount",
66    "ManagerAccountCollection",
67    "ManagerCollection",
68    "ManagerDiagnosticData",
69    "ManagerNetworkProtocol",
70    "Memory",
71    "MemoryCollection",
72    "Message",
73    "MessageRegistry",
74    "MessageRegistryCollection",
75    "MessageRegistryFile",
76    "MessageRegistryFileCollection",
77    "MetricDefinition",
78    "MetricDefinitionCollection",
79    "MetricReport",
80    "MetricReportCollection",
81    "MetricReportDefinition",
82    "MetricReportDefinitionCollection",
83    "OperatingConfig",
84    "OperatingConfigCollection",
85    "PCIeDevice",
86    "PCIeDeviceCollection",
87    "PCIeFunction",
88    "PCIeFunctionCollection",
89    "PhysicalContext",
90    "PCIeSlots",
91    "Port",
92    "PortCollection",
93    "Power",
94    "PowerSubsystem",
95    "PowerSupply",
96    "PowerSupplyCollection",
97    "Privileges",  # Used in Role
98    "Processor",
99    "ProcessorCollection",
100    "RedfishError",
101    "RedfishExtensions",
102    "Redundancy",
103    "Resource",
104    "Role",
105    "RoleCollection",
106    "Sensor",
107    "SensorCollection",
108    "ServiceRoot",
109    "Session",
110    "SessionCollection",
111    "SessionService",
112    "Settings",
113    "SoftwareInventory",
114    "SoftwareInventoryCollection",
115    "Storage",
116    "StorageCollection",
117    "StorageController",
118    "StorageControllerCollection",
119    "Task",
120    "TaskCollection",
121    "TaskService",
122    "TelemetryService",
123    "Thermal",
124    "ThermalMetrics",
125    "ThermalSubsystem",
126    "Triggers",
127    "TriggersCollection",
128    "UpdateService",
129    "VLanNetworkInterfaceCollection",
130    "VLanNetworkInterface",
131    "VirtualMedia",
132    "VirtualMediaCollection",
133    "odata",
134    "odata-v4",
135    "redfish-error",
136    "redfish-payload-annotations",
137    "redfish-schema",
138    "redfish-schema-v1",
139]
140
141SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
142
143proxies = {"https": os.environ.get("https_proxy", None)}
144
145r = requests.get(
146    "https://www.dmtf.org/sites/default/files/standards/documents/"
147    + VERSION
148    + ".zip",
149    proxies=proxies,
150)
151
152r.raise_for_status()
153
154
155static_path = os.path.realpath(
156    os.path.join(SCRIPT_DIR, "..", "static", "redfish", "v1")
157)
158
159
160cpp_path = os.path.realpath(
161    os.path.join(SCRIPT_DIR, "..", "redfish-core", "include")
162)
163
164
165schema_path = os.path.join(static_path, "schema")
166json_schema_path = os.path.join(static_path, "JsonSchemas")
167metadata_index_path = os.path.join(static_path, "$metadata", "index.xml")
168
169zipBytesIO = BytesIO(r.content)
170zip_ref = zipfile.ZipFile(zipBytesIO)
171
172
173class SchemaVersion:
174    """
175    A Python class for sorting Redfish schema versions.  Allows sorting Redfish
176    versions in the way humans expect, by comparing version strings as lists
177    (ie 0_2_0 comes before 0_10_0) in the way humans expect.  It does case
178    insensitive schema name comparisons
179    """
180
181    def __init__(self, key):
182        key = str.casefold(key)
183
184        split_tup = key.split(".")
185        self.version_pieces = [split_tup[0]]
186        if len(split_tup) < 2:
187            return
188        version = split_tup[1]
189
190        if version.startswith("v"):
191            version = version[1:]
192        if any(char.isdigit() for char in version):
193            self.version_pieces.extend([int(x) for x in version.split("_")])
194
195    def __lt__(self, other):
196        return self.version_pieces < other.version_pieces
197
198
199# Remove the old files
200skip_prefixes = ["Oem", "OpenBMC"]
201if os.path.exists(schema_path):
202    files = [
203        os.path.join(schema_path, f)
204        for f in os.listdir(schema_path)
205        if not any([f.startswith(prefix) for prefix in skip_prefixes])
206    ]
207    for f in files:
208        os.remove(f)
209if os.path.exists(json_schema_path):
210    files = [
211        os.path.join(json_schema_path, f)
212        for f in os.listdir(json_schema_path)
213        if not any([f.startswith(prefix) for prefix in skip_prefixes])
214    ]
215    for f in files:
216        if os.path.isfile(f):
217            os.remove(f)
218        else:
219            shutil.rmtree(f)
220try:
221    os.remove(metadata_index_path)
222except FileNotFoundError:
223    pass
224
225if not os.path.exists(schema_path):
226    os.makedirs(schema_path)
227if not os.path.exists(json_schema_path):
228    os.makedirs(json_schema_path)
229
230csdl_filenames = []
231json_schema_files = defaultdict(list)
232
233for zip_file in zip_ref.infolist():
234    if zip_file.is_dir():
235        continue
236    if zip_file.filename.startswith("csdl/"):
237        csdl_filenames.append(os.path.basename(zip_file.filename))
238    elif zip_file.filename.startswith("json-schema/"):
239        filename = os.path.basename(zip_file.filename)
240        filenamesplit = filename.split(".")
241        # exclude schemas again to save flash space
242        if filenamesplit[0] not in include_list:
243            continue
244        json_schema_files[filenamesplit[0]].append(filename)
245    elif zip_file.filename.startswith("openapi/"):
246        pass
247    elif zip_file.filename.startswith("dictionaries/"):
248        pass
249
250# sort the json files by version
251for key, value in json_schema_files.items():
252    value.sort(key=SchemaVersion, reverse=True)
253
254# Create a dictionary ordered by schema name
255json_schema_files = OrderedDict(
256    sorted(json_schema_files.items(), key=lambda x: SchemaVersion(x[0]))
257)
258
259csdl_filenames.sort(key=SchemaVersion)
260with open(metadata_index_path, "w") as metadata_index:
261    metadata_index.write('<?xml version="1.0" encoding="UTF-8"?>\n')
262    metadata_index.write(
263        "<edmx:Edmx xmlns:edmx="
264        '"http://docs.oasis-open.org/odata/ns/edmx"'
265        ' Version="4.0">\n'
266    )
267
268    for filename in csdl_filenames:
269        # filename looks like Zone_v1.xml
270        with open(os.path.join(schema_path, filename), "wb") as schema_out:
271            content = zip_ref.read(os.path.join("csdl", filename))
272            content = content.replace(b"\r\n", b"\n")
273
274            schema_out.write(content)
275
276            filenamesplit = filename.split("_")
277            if filenamesplit[0] not in include_list:
278                continue
279            metadata_index.write(
280                '    <edmx:Reference Uri="/redfish/v1/schema/'
281                + filename
282                + '">\n'
283            )
284
285            xml_root = ET.fromstring(content)
286            edmx = "{http://docs.oasis-open.org/odata/ns/edmx}"
287            edm = "{http://docs.oasis-open.org/odata/ns/edm}"
288            for edmx_child in xml_root:
289                if edmx_child.tag == edmx + "DataServices":
290                    for data_child in edmx_child:
291                        if data_child.tag == edm + "Schema":
292                            namespace = data_child.attrib["Namespace"]
293                            if namespace.startswith("RedfishExtensions"):
294                                metadata_index.write(
295                                    '        <edmx:Include Namespace="'
296                                    + namespace
297                                    + '"  Alias="Redfish"/>\n'
298                                )
299
300                            else:
301                                metadata_index.write(
302                                    '        <edmx:Include Namespace="'
303                                    + namespace
304                                    + '"/>\n'
305                                )
306            metadata_index.write("    </edmx:Reference>\n")
307
308    metadata_index.write(
309        "    <edmx:DataServices>\n"
310        "        <Schema "
311        'xmlns="http://docs.oasis-open.org/odata/ns/edm" '
312        'Namespace="Service">\n'
313        '            <EntityContainer Name="Service" '
314        'Extends="ServiceRoot.v1_0_0.ServiceContainer"/>\n'
315        "        </Schema>\n"
316        "    </edmx:DataServices>\n"
317    )
318    # TODO:Issue#32 There's a bug in the script that currently deletes this
319    # schema (because it's an OEM schema). Because it's the only six, and we
320    # don't update schemas very often, we just manually fix it. Need a
321    # permanent fix to the script.
322    metadata_index.write(
323        '    <edmx:Reference Uri="/redfish/v1/schema/OemManager_v1.xml">\n'
324    )
325    metadata_index.write('        <edmx:Include Namespace="OemManager"/>\n')
326    metadata_index.write("    </edmx:Reference>\n")
327
328    metadata_index.write(
329        '    <edmx:Reference Uri="'
330        '/redfish/v1/schema/OemComputerSystem_v1.xml">\n'
331    )
332    metadata_index.write(
333        '        <edmx:Include Namespace="OemComputerSystem"/>\n'
334    )
335    metadata_index.write("    </edmx:Reference>\n")
336
337    metadata_index.write(
338        '    <edmx:Reference Uri="'
339        '/redfish/v1/schema/OemVirtualMedia_v1.xml">\n'
340    )
341    metadata_index.write(
342        '        <edmx:Include Namespace="OemVirtualMedia"/>\n'
343    )
344    metadata_index.write(
345        '        <edmx:Include Namespace="OemVirtualMedia.v1_0_0"/>\n'
346    )
347    metadata_index.write("    </edmx:Reference>\n")
348
349    metadata_index.write(
350        '    <edmx:Reference Uri="'
351        '/redfish/v1/schema/OpenBMCAccountService_v1.xml">\n'
352    )
353    metadata_index.write(
354        '        <edmx:Include Namespace="OpenBMCAccountService"/>\n'
355    )
356    metadata_index.write(
357        '        <edmx:Include Namespace="OpenBMCAccountService.v1_0_0"/>\n'
358    )
359    metadata_index.write("    </edmx:Reference>\n")
360
361    metadata_index.write("</edmx:Edmx>\n")
362
363
364for schema, version in json_schema_files.items():
365    zip_filepath = os.path.join("json-schema", version[0])
366    schemadir = os.path.join(json_schema_path, schema)
367    os.makedirs(schemadir)
368
369    with open(os.path.join(schemadir, schema + ".json"), "wb") as schema_file:
370        schema_file.write(zip_ref.read(zip_filepath).replace(b"\r\n", b"\n"))
371
372with open(os.path.join(cpp_path, "schemas.hpp"), "w") as hpp_file:
373    hpp_file.write(
374        "#pragma once\n"
375        "{WARNING}\n"
376        "// clang-format off\n"
377        "#include <array>\n"
378        "\n"
379        "namespace redfish\n"
380        "{{\n"
381        "    constexpr std::array schemas {{\n".format(WARNING=WARNING)
382    )
383    for schema_file in json_schema_files:
384        hpp_file.write('        "{}",\n'.format(schema_file))
385
386    hpp_file.write("    };\n}\n")
387
388zip_ref.close()
389
390generate_schema_enums.main()
391generate_top_collections()
392
393# Now delete the xml schema files we aren't supporting
394if os.path.exists(schema_path):
395    files = [
396        os.path.join(schema_path, f)
397        for f in os.listdir(schema_path)
398        if not any([f.startswith(prefix) for prefix in skip_prefixes])
399    ]
400    for filename in files:
401        # filename will include the absolute path
402        filenamesplit = filename.split("/")
403        name = filenamesplit.pop()
404        namesplit = name.split("_")
405        if namesplit[0] not in include_list:
406            print("excluding schema: " + filename)
407            os.remove(filename)
408