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:
parent
d28a61b2c0
commit
be5c4b7d63
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||||
|
187
doc/source/_exts/redfish_interop.py
Normal file
187
doc/source/_exts/redfish_interop.py
Normal 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__}
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user