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