From 56066a20a476d33c8732279bed74cd8514b198e7 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 26 Jun 2020 15:14:01 +1200 Subject: [PATCH] Document metalsmith_instances Change-Id: I9d3d17f104caea470252d400d26c225ca5de3b6e --- doc/requirements.txt | 1 + doc/source/_exts/ansible-autodoc.py | 264 ++++++++++++++++++ doc/source/conf.py | 4 + doc/source/user/ansible.rst | 8 + .../modules/metalsmith_instances.py | 20 ++ .../roles/metalsmith_deployment/README.rst | 4 +- 6 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 doc/source/_exts/ansible-autodoc.py diff --git a/doc/requirements.txt b/doc/requirements.txt index 65e48d8..ad15e51 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -6,3 +6,4 @@ sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 +ansible>=2.8 diff --git a/doc/source/_exts/ansible-autodoc.py b/doc/source/_exts/ansible-autodoc.py new file mode 100644 index 0000000..58cfd2d --- /dev/null +++ b/doc/source/_exts/ansible-autodoc.py @@ -0,0 +1,264 @@ +# Copyright 2019 Red Hat, Inc. +# 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 imp +import os + +from docutils import core +from docutils import nodes +from docutils.parsers import rst +from docutils.parsers.rst import Directive +from docutils.writers.html4css1 import Writer + +import yaml + + +DOCYAML = yaml +DOCYAML.default_flow_style = False + + +class AnsibleAutoPluginDirective(Directive): + directive_name = "ansibleautoplugin" + has_content = True + option_spec = { + 'module': rst.directives.unchanged, + 'role': rst.directives.unchanged, + 'documentation': rst.directives.unchanged, + 'examples': rst.directives.unchanged + } + + @staticmethod + def _render_html(source): + return core.publish_parts( + source=source, + writer=Writer(), + writer_name='html', + settings_overrides={'no_system_messages': True} + ) + + def make_node(self, title, contents, content_type=None): + section = self._section_block(title=title) + if not content_type: + # Doc section + for content in contents['docs']: + for paragraph in content.split('\n'): + retnode = nodes.paragraph() + retnode.append(self._raw_html_block(data=paragraph)) + section.append(retnode) + + # Options Section + options_list = nodes.field_list() + options_section = self._section_block(title='Options') + for key, value in contents['options'].items(): + options_list.append( + self._raw_fields( + data=value['description'], + field_name=key + ) + ) + else: + options_section.append(options_list) + section.append(options_section) + + # Authors Section + authors_list = nodes.field_list() + authors_list.append( + self._raw_fields( + data=contents['author'] + ) + ) + authors_section = self._section_block(title='Authors') + authors_section.append(authors_list) + section.append(authors_section) + + elif content_type == 'yaml': + for content in contents: + section.append( + self._literal_block( + data=content, + dump_data=False + ) + ) + + return section + + @staticmethod + def load_module(filename): + return imp.load_source('__ansible_module__', filename) + + @staticmethod + def build_documentation(module): + docs = DOCYAML.load(module.DOCUMENTATION) + doc_data = dict() + doc_data['docs'] = docs['description'] + doc_data['author'] = docs.get('author', list()) + doc_data['options'] = docs.get('options', dict()) + return doc_data + + @staticmethod + def build_examples(module): + examples = DOCYAML.load(module.EXAMPLES) + return_examples = list() + for example in examples: + return_examples.append(DOCYAML.dump([example], sort_keys=False)) + return return_examples + + def _raw_html_block(self, data): + html = self._render_html(source=data) + return nodes.raw('', html['body'], format='html') + + def _raw_fields(self, data, field_name=''): + body = nodes.field_body() + if isinstance(data, list): + for item in data: + body.append(self._raw_html_block(data=item)) + else: + body.append(self._raw_html_block(data=data)) + + field = nodes.field() + field.append(nodes.field_name(text=field_name)) + field.append(body) + return field + + @staticmethod + def _literal_block(data, language='yaml', dump_data=True): + if dump_data: + literal = nodes.literal_block( + text=DOCYAML.dump(data) + ) + else: + literal = nodes.literal_block(text=data) + literal['language'] = 'yaml' + return literal + + @staticmethod + def _section_block(title, text=None): + section = nodes.section( + title, + nodes.title(text=title), + ids=[nodes.make_id('-'.join(title))], + ) + if text: + section_body = nodes.field_body() + section_body.append(nodes.paragraph(text=text)) + section.append(section_body) + + return section + + def _yaml_section(self, to_yaml_data, section_title, section_text=None): + yaml_section = self._section_block( + title=section_title, + text=section_text + ) + yaml_section.append(self._literal_block(data=to_yaml_data)) + return yaml_section + + def _run_role(self, role): + section = self._section_block( + title='Role Documentation', + text='Welcome to the "{}" role documentation.'.format( + os.path.basename(role) + ) + ) + defaults_file = os.path.join(role, 'defaults', 'main.yml') + if os.path.exists(defaults_file): + with open(defaults_file) as f: + role_defaults = DOCYAML.load(f.read()) + section.append( + self._yaml_section( + to_yaml_data=role_defaults, + section_title='Role Defaults', + section_text='This section highlights all of the defaults' + ' and variables set within the "{}"' + ' role.'.format(os.path.basename(role)) + ) + ) + + vars_path = os.path.join(role, 'vars') + if os.path.exists(vars_path): + for v_file in os.listdir(vars_path): + vars_file = os.path.join(vars_path, v_file) + with open(vars_file) as f: + vars_values = DOCYAML.load(f.read()) + section.append( + self._yaml_section( + to_yaml_data=vars_values, + section_title='Role Variables: {}'.format(v_file) + ) + ) + + self.run_returns.append(section) + + # Document any libraries nested within the role + library_path = os.path.join(role, 'library') + if os.path.exists(library_path): + self.options['documentation'] = True + self.options['examples'] = True + for lib in os.listdir(library_path): + if lib.endswith('.py'): + self._run_module( + module=self.load_module( + filename=os.path.join( + library_path, + lib + ) + ), + module_title='Embedded module: {}'.format(lib), + example_title='Examples for embedded module' + ) + + def _run_module(self, module, module_title="Module Documentation", + example_title="Example Tasks"): + if self.options.get('documentation'): + docs = self.build_documentation(module=module) + self.run_returns.append( + self.make_node( + title=module_title, + contents=docs + ) + ) + + if self.options.get('examples'): + examples = self.build_examples(module=module) + self.run_returns.append( + self.make_node( + title=example_title, + contents=examples, + content_type='yaml' + ) + ) + + def run(self): + self.run_returns = list() + + if self.options.get('module'): + module = self.load_module(filename=self.options['module']) + self._run_module(module=module) + + if self.options.get('role'): + self._run_role(role=self.options['role']) + + return self.run_returns + + +def setup(app): + classes = [ + AnsibleAutoPluginDirective, + ] + for directive_class in classes: + app.add_directive(directive_class.directive_name, directive_class) + + return {'version': '0.2'} diff --git a/doc/source/conf.py b/doc/source/conf.py index 0482bc8..942ab69 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -14,7 +14,10 @@ import os import sys +# Add the project sys.path.insert(0, os.path.abspath('../..')) +# Add the extensions +sys.path.insert(0, os.path.join(os.path.abspath('.'), '_exts')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be @@ -23,6 +26,7 @@ extensions = [ 'sphinxcontrib.apidoc', 'sphinxcontrib.rsvgconverter', 'openstackdocstheme', + 'ansible-autodoc' ] autoclass_content = 'both' diff --git a/doc/source/user/ansible.rst b/doc/source/user/ansible.rst index 26dccdc..2c79c42 100644 --- a/doc/source/user/ansible.rst +++ b/doc/source/user/ansible.rst @@ -1 +1,9 @@ .. include:: ../../../metalsmith_ansible/roles/metalsmith_deployment/README.rst + +Module - metalsmith_instances +============================= + +.. ansibleautoplugin:: + :module: metalsmith_ansible/ansible_plugins/modules/metalsmith_instances.py + :documentation: true + :examples: true diff --git a/metalsmith_ansible/ansible_plugins/modules/metalsmith_instances.py b/metalsmith_ansible/ansible_plugins/modules/metalsmith_instances.py index 9256cc8..a8d75f7 100644 --- a/metalsmith_ansible/ansible_plugins/modules/metalsmith_instances.py +++ b/metalsmith_ansible/ansible_plugins/modules/metalsmith_instances.py @@ -186,6 +186,26 @@ options: - error ''' +EXAMPLES = ''' +- name: Provision instances + metalsmith_instances: + instances: + - name: node-0 + hostname: compute-0 + image: overcloud-full + state: present + wait: true + clean_up: false + timeout: 1200 + concurrency: 20 + log_level: info + register: baremetal_provisioned + +- name: Metalsmith log for provision instances + debug: + var: baremetal_provisioned.logging +''' + METALSMITH_LOG_MAP = { 'debug': logging.DEBUG, diff --git a/metalsmith_ansible/roles/metalsmith_deployment/README.rst b/metalsmith_ansible/roles/metalsmith_deployment/README.rst index 2f20ca3..581c80c 100644 --- a/metalsmith_ansible/roles/metalsmith_deployment/README.rst +++ b/metalsmith_ansible/roles/metalsmith_deployment/README.rst @@ -1,5 +1,5 @@ -Metalsmith Deployment -===================== +Role - metalsmith_deployment +============================ This role deploys instances using **metalsmith** CLI.