Render the redfish interop profile in the docs

Adds a pretty straightforward Sphinx plugin that reads the JSON profile
file and renders it nicely in a document that is then included from
the Redfish page.

Change-Id: Ic2da61cb510897eac8a2e162816cfd05cc22994c
This commit is contained in:
Dmitry Tantsur 2024-05-27 18:44:03 +02:00
parent d28a61b2c0
commit be5c4b7d63
No known key found for this signature in database
GPG Key ID: 315B2AF9FD216C60
4 changed files with 198 additions and 2 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
_build _build
doc/source/contributor/api/ doc/source/contributor/api/
_static _static
doc/source/admin/drivers/redfish/OpenStackIronicProfile.*.rst
# release notes build # release notes build
releasenotes/build releasenotes/build

View File

@ -0,0 +1,187 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
from sphinx.application import Sphinx
__version__ = "1.0.0"
# Data model #
class Entity:
"""Represents an entity in the profile."""
def __init__(self, name, src):
self.name = name
self.src = src
self.purpose = src.get('Purpose', '')
self.writable = src.get('WriteRequirement') == 'Mandatory'
self.required = (src.get('ReadRequirement') in ('Mandatory', None)
or self.writable)
class ActionParameter(Entity):
"""Represents a parameter in an Action."""
def __init__(self, name, src):
super().__init__(name, src)
self.required_values = src.get('ParameterValues') or []
self.recommended_values = src.get('RecommendedValues') or []
class Action(Entity):
"""Represents an action on a resource."""
def __init__(self, name, src):
super().__init__(name, src)
self.parameters = {
name: ActionParameter(name, value)
for name, value in src.get('Parameters', {}).items()
}
class Resource(Entity):
"""Represents any resource in the profile.
Both top-level resources and nested fields are represented by this class
(but actions are not).
"""
def __init__(self, name, src):
super().__init__(name, src)
self.min_support_values = src.get('MinSupportValues')
self.properties = {
name: Resource(name, value)
for name, value in src.get('PropertyRequirements', {}).items()
}
self.actions = {
name: Action(name, value)
for name, value in src.get('ActionRequirements', {}).items()
}
self.link_to = (src['Values'][0]
if src.get('Comparison') == 'LinkToResource'
else None)
# Rendering #
LEVELS = {0: '=', 1: '-', 2: '~', 3: '^'}
INDENT = ' ' * 4
class NestedWriter:
"""A writer that is nested with indentations."""
def __init__(self, dest, level=0):
self.dest = dest
self.level = level
def text(self, text):
print(INDENT * self.level + text, file=self.dest)
def para(self, text):
self.text(text)
print(file=self.dest)
def _nested_common(self, res):
required = " **[required]**" if res.required else ""
writable = " **[writable]**" if res.writable else ""
self.text(f"``{res.name}``{required}{writable}")
nested = NestedWriter(self.dest, self.level + 1)
if res.purpose:
nested.para(res.purpose)
return nested
def action(self, res):
nested = self._nested_common(res)
for prop in res.parameters.values():
nested.action_parameter(prop)
print(file=self.dest)
def action_parameter(self, res):
self._nested_common(res)
print(file=self.dest)
def resource(self, res):
nested = self._nested_common(res)
for prop in res.properties.values():
nested.resource(prop)
if res.link_to:
# NOTE(dtantsur): this is a bit hacky, but we don't have
# definitions for all possible collections.
split = res.link_to.split('Collection')
if len(split) > 1:
nested.text("Link to a collection of "
f":ref:`Redfish-{split[0]}` resources.")
else:
nested.text(f"Link to a :ref:`Redfish-{res.link_to}` "
"resource.")
print(file=self.dest)
class Writer(NestedWriter):
def __init__(self, dest):
super().__init__(dest)
def title(self, text, level=1):
print(text, file=self.dest)
print(LEVELS[level] * len(text), file=self.dest)
def top_level(self, res):
required = " **[required]**" if res.required else ""
self.para(f".. _Redfish-{res.name}:")
self.title(f"{res.name}")
self.para(f"{res.purpose}{required}")
if res.properties:
self.title("Properties", level=2)
for name, prop in res.properties.items():
self.resource(prop)
if res.actions:
self.title("Actions", level=2)
for name, act in res.actions.items():
self.action(act)
def builder_inited(app: Sphinx):
source = os.path.join(app.srcdir, app.config.redfish_interop_source)
with open(source) as fp:
profile = json.load(fp)
fname = os.path.basename(source).replace('json', 'rst')
dstdir = os.path.join(app.srcdir, app.config.redfish_interop_output_dir)
with open(os.path.join(dstdir, fname), 'wt') as dest:
w = Writer(dest)
w.title(f"{profile['ProfileName']} {profile['ProfileVersion']}", 0)
w.para(profile['Purpose'])
try:
for name, value in sorted(
(name, value)
for name, value in profile['Resources'].items()
):
w.top_level(Resource(name, value))
except Exception:
import traceback
traceback.print_exc()
raise
def setup(app: Sphinx):
app.connect('builder-inited', builder_inited)
app.add_config_value('redfish_interop_source', None, 'env', [str])
app.add_config_value('redfish_interop_output_dir', None, 'env', [str])
return {'version': __version__}

View File

@ -18,6 +18,10 @@ and conformance testing. Many of the properties defined within this structure
have assumed default values that correspond with the most common use case, so have assumed default values that correspond with the most common use case, so
that those properties can be omitted from the document for brevity. that those properties can be omitted from the document for brevity.
.. toctree::
OpenStackIronicProfile.v1_1_0
Validation of Profiles using DMTF tool Validation of Profiles using DMTF tool
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -27,4 +31,3 @@ Redfish Interoperability Profile. The Redfish Interop Validator is available
for download from the DMTF's organization on Github at for download from the DMTF's organization on Github at
https://github.com/DMTF/Redfish-Interop-Validator. Refer to instructions in https://github.com/DMTF/Redfish-Interop-Validator. Refer to instructions in
README on how to configure and run validation. README on how to configure and run validation.

View File

@ -42,7 +42,8 @@ extensions = ['sphinx.ext.viewcode',
'oslo_policy.sphinxpolicygen', 'oslo_policy.sphinxpolicygen',
'automated_steps', 'automated_steps',
'openstackdocstheme', 'openstackdocstheme',
'web_api_docstring' 'web_api_docstring',
'redfish_interop',
] ]
# sphinxcontrib.apidoc options # sphinxcontrib.apidoc options
@ -61,6 +62,10 @@ autodoc_default_options = {
'special-members': '__call__', 'special-members': '__call__',
} }
redfish_interop_source = \
'../../redfish-interop-profiles/OpenStackIronicProfile.v1_1_0.json'
redfish_interop_output_dir = 'admin/drivers/redfish/'
openstackdocs_repo_name = 'openstack/ironic' openstackdocs_repo_name = 'openstack/ironic'
openstackdocs_use_storyboard = False openstackdocs_use_storyboard = False
openstackdocs_pdf_link = True openstackdocs_pdf_link = True