# 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. import inspect 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__) # Enable this locally if you need debugging output DEBUG = False 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 line in lines[1:]: add(' %s' % line) add('') def _format_doc(doc): "Format one method docstring to be shown in the step table." paras = doc.split('\n\n') formatted_docstring = [] for line in paras: if line.startswith(':'): continue # Remove the field table that commonly appears at the end of a # docstring. formatted_docstring.append(line) return '\n\n'.join(formatted_docstring) _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): if DEBUG: LOG.info('[%s] probing available plugins for interface %s', __name__, interface_name) loader = stevedore.ExtensionManager( 'ironic.hardware.interfaces.{}'.format(interface_name), invoke_on_load=False, ) for plugin in loader: if plugin.name == 'fake': continue 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)), } if DEBUG: LOG.info('[%s] interface %r driver %r STEP %r', __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', 'firmware', '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()