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