ironic/doc/source/_exts/redfish_interop.py
Dmitry Tantsur be5c4b7d63
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
2024-06-27 16:32:28 +02:00

188 lines
5.8 KiB
Python

# 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__}