From e21aad3fd249b68e9abfb72e0a4875aae2c67661 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 30 Aug 2019 15:23:43 -0400 Subject: [PATCH] add table of available cleaning steps to documentation The documentation currently has no information at all about what steps are available *generally* for automated or manual cleaning. Users who want to find this information must review the source code themselves, except in one or two cases where driver-specific documentation has the details spelled out. This patch adds a series of tables to the general cleaning documentation to provide details about the steps one could possibly add to the instructions given to ironic, along with details about required parameters and what the effect of each step will be. Change-Id: I6b225cce38b815248c457814508531041c437e6d Signed-off-by: Doug Hellmann --- doc/source/_exts/automated_steps.py | 187 ++++++++++++++++++++++++++++ doc/source/admin/cleaning.rst | 2 + doc/source/conf.py | 9 ++ tox.ini | 1 + 4 files changed, 199 insertions(+) create mode 100644 doc/source/_exts/automated_steps.py diff --git a/doc/source/_exts/automated_steps.py b/doc/source/_exts/automated_steps.py new file mode 100644 index 0000000000..e61d356bd0 --- /dev/null +++ b/doc/source/_exts/automated_steps.py @@ -0,0 +1,187 @@ +# All Rights Reserved. +# +# 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. + +from collections import defaultdict +import inspect +import itertools +import operator +import os.path + +from docutils import nodes +from docutils.parsers import rst +from docutils.parsers.rst import directives +from docutils.statemachine import ViewList +from sphinx.util import logging +from sphinx.util.nodes import nested_parse_with_titles +import stevedore + +from ironic.common import driver_factory + +LOG = logging.getLogger(__name__) + + +def _list_table(add, headers, data, title='', columns=None): + """Build a list-table directive. + + :param add: Function to add one row to output. + :param headers: List of header values. + :param data: Iterable of row data, yielding lists or tuples with rows. + """ + add('.. list-table:: %s' % title) + add(' :header-rows: 1') + if columns: + add(' :widths: %s' % (','.join(str(c) for c in columns))) + add('') + add(' - * %s' % headers[0]) + for h in headers[1:]: + add(' * %s' % h) + for row in data: + add(' - * %s' % row[0]) + for r in row[1:]: + lines = str(r).splitlines() + if not lines: + # empty string + add(' * ') + else: + # potentially multi-line string + add(' * %s' % lines[0]) + for l in lines[1:]: + add(' %s' % l) + add('') + + +def _format_doc(doc): + "Format one method docstring to be shown in the step table." + paras = doc.split('\n\n') + if paras[-1].startswith(':'): + # Remove the field table that commonly appears at the end of a + # docstring. + paras = paras[:-1] + return '\n\n'.join(paras) + + +_clean_steps = {} + + +def _init_steps_by_driver(): + "Load step information from drivers." + + # NOTE(dhellmann): This reproduces some of the logic of + # ironic.drivers.base.BaseInterface.__new__ and + # ironic.common.driver_factory but does so without + # instantiating the interface classes, which means that if + # some of the preconditions aren't met we can still inspect + # the methods of the class. + + for interface_name in sorted(driver_factory.driver_base.ALL_INTERFACES): + LOG.info('[{}] probing available plugins for interface {}'.format( + __name__, interface_name)) + + loader = stevedore.ExtensionManager( + 'ironic.hardware.interfaces.{}'.format(interface_name), + invoke_on_load=False, + ) + + for plugin in loader: + steps = [] + + for method_name, method in inspect.getmembers(plugin.plugin): + if not getattr(method, '_is_clean_step', False): + continue + step = { + 'step': method.__name__, + 'priority': method._clean_step_priority, + 'abortable': method._clean_step_abortable, + 'argsinfo': method._clean_step_argsinfo, + 'interface': interface_name, + 'doc': _format_doc(inspect.getdoc(method)), + } + LOG.info('[{}] interface {!r} driver {!r} STEP {}'.format( + __name__, interface_name, plugin.name, step)) + steps.append(step) + + if steps: + if interface_name not in _clean_steps: + _clean_steps[interface_name] = {} + _clean_steps[interface_name][plugin.name] = steps + + +def _format_args(argsinfo): + argsinfo = argsinfo or {} + return '\n\n'.join( + '``{}``{}{} {}'.format( + argname, + ' (*required*)' if argdetail.get('required') else '', + ' --' if argdetail.get('description') else '', + argdetail.get('description', ''), + ) + for argname, argdetail in sorted(argsinfo.items()) + ) + + +class AutomatedStepsDirective(rst.Directive): + + option_spec = { + 'phase': directives.unchanged, + } + + def run(self): + series = self.options.get('series', 'cleaning') + + if series != 'cleaning': + raise NotImplementedError('Showing deploy steps not implemented') + + source_name = '<{}>'.format(__name__) + + result = ViewList() + + for interface_name in ['power', 'management', 'deploy', 'bios', 'raid']: + interface_info = _clean_steps.get(interface_name, {}) + if not interface_info: + continue + + title = '{} Interface'.format(interface_name.capitalize()) + result.append(title, source_name) + result.append('~' * len(title), source_name) + + for driver_name, steps in sorted(interface_info.items()): + + _list_table( + title='{} cleaning steps'.format(driver_name), + add=lambda x: result.append(x, source_name), + headers=['Name', 'Details', 'Priority', 'Stoppable', 'Arguments'], + columns=[20, 30, 10, 10, 30], + data=( + ('``{}``'.format(s['step']), + s['doc'], + s['priority'], + 'yes' if s['abortable'] else 'no', + _format_args(s['argsinfo']), + ) + for s in steps + ), + ) + + # NOTE(dhellmann): Useful for debugging. + print('\n'.join(result)) + + node = nodes.section() + node.document = self.state.document + nested_parse_with_titles(self.state, result, node) + return node.children + + +def setup(app): + app.add_directive('show-steps', AutomatedStepsDirective) + _init_steps_by_driver() diff --git a/doc/source/admin/cleaning.rst b/doc/source/admin/cleaning.rst index 5960c0f3f7..67c6c9e9fc 100644 --- a/doc/source/admin/cleaning.rst +++ b/doc/source/admin/cleaning.rst @@ -73,6 +73,8 @@ cleaning steps. See `How do I change the priority of a cleaning step?`_ for more information. +.. show-steps:: + :phase: cleaning .. _manual_cleaning: diff --git a/doc/source/conf.py b/doc/source/conf.py index 20b2b0ab52..9e6be5247a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,6 +11,9 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import sys + import eventlet # NOTE(dims): monkey patch subprocess to prevent failures in latest eventlet @@ -22,6 +25,11 @@ except TypeError: # -- General configuration ---------------------------------------------------- +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.join(os.path.abspath('.'), '_exts')) + # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.viewcode', @@ -34,6 +42,7 @@ extensions = ['sphinx.ext.viewcode', 'oslo_config.sphinxconfiggen', 'oslo_policy.sphinxext', 'oslo_policy.sphinxpolicygen', + 'automated_steps', ] try: diff --git a/tox.ini b/tox.ini index 7537fcfa35..14ec49fd11 100644 --- a/tox.ini +++ b/tox.ini @@ -85,6 +85,7 @@ deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt + -r{toxinidir}/driver-requirements.txt commands = sphinx-build -b html -W doc/source doc/build/html [testenv:api-ref]