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]