diff options
author | Brian Coca <brian.coca+git@gmail.com> | 2015-04-30 22:29:12 -0400 |
---|---|---|
committer | Brian Coca <brian.coca+git@gmail.com> | 2015-04-30 22:33:29 -0400 |
commit | 38d2042739dd3c2c295ecf11267ebcc07bce5bf4 (patch) | |
tree | 98989c6917fc87ad6bc1be6f3240bd3b5ef1e821 | |
parent | df881b7f37bb53287c504f0180ad2813eaf36e03 (diff) | |
download | ansible-38d2042739dd3c2c295ecf11267ebcc07bce5bf4.tar.gz |
v2 ansible-doc can now list modules
-rw-r--r-- | v2/ansible/cli/doc.py | 114 | ||||
-rw-r--r-- | v2/ansible/utils/module_docs.py | 102 | ||||
l--------- | v2/ansible/utils/module_docs_fragments | 1 |
3 files changed, 202 insertions, 15 deletions
diff --git a/v2/ansible/cli/doc.py b/v2/ansible/cli/doc.py index ec09cb158d..f77ccf67da 100644 --- a/v2/ansible/cli/doc.py +++ b/v2/ansible/cli/doc.py @@ -16,14 +16,19 @@ # ansible-vault is a script that encrypts/decrypts YAML files. See # http://docs.ansible.com/playbooks_vault.html for more details. +import fcntl import os +import re +import struct import sys +import termios import traceback from ansible import constants as C from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.plugins import module_loader from ansible.cli import CLI -#from ansible.utils import module_docs +from ansible.utils import module_docs class DocCLI(CLI): """ Vault command line class """ @@ -41,13 +46,16 @@ class DocCLI(CLI): LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars) # -S (chop long lines) -X (disable termcap init and de-init) + def __init__(self, args, display=None): + + super(DocCLI, self).__init__(args, display) + self.module_list = [] def parse(self): - self.parser = optparse.OptionParser( - version=version("%prog"), + self.parser = CLI.base_parser( usage='usage: %prog [options] [module...]', - description='Show Ansible module documentation', + epilog='Show Ansible module documentation', ) self.parser.add_option("-M", "--module-path", action="store", dest="module_path", default=C.DEFAULT_MODULE_PATH, @@ -56,8 +64,6 @@ class DocCLI(CLI): help='List available modules') self.parser.add_option("-s", "--snippet", action="store_true", default=False, dest='show_snippet', help='Show playbook snippet for specified module(s)') - self.parser.add_option('-v', action='version', help='Show version number and exit') - self.options, self.args = self.parser.parse_args() self.display.verbosity = self.options.verbosity @@ -65,19 +71,97 @@ class DocCLI(CLI): def run(self): - if options.module_path is not None: - for i in options.module_path.split(os.pathsep): - utils.plugins.module_finder.add_directory(i) + if self.options.module_path is not None: + for i in self.options.module_path.split(os.pathsep): + module_loader.add_directory(i) - if options.list_dir: + if self.options.list_dir: # list modules - paths = utils.plugins.module_finder._get_paths() - module_list = [] + paths = module_loader._get_paths() for path in paths: - find_modules(path, module_list) + self.find_modules(path) - pager(get_module_list_text(module_list)) + #self.pager(get_module_list_text(module_list)) + print self.get_module_list_text() + return 0 - if len(args) == 0: + if len(self.args) == 0: raise AnsibleOptionsError("Incorrect options passed") + + def find_modules(self, path): + + if os.path.isdir(path): + for module in os.listdir(path): + if module.startswith('.'): + continue + elif os.path.isdir(module): + self.find_modules(module) + elif any(module.endswith(x) for x in self.BLACKLIST_EXTS): + continue + elif module.startswith('__'): + continue + elif module in self.IGNORE_FILES: + continue + elif module.startswith('_'): + fullpath = '/'.join([path,module]) + if os.path.islink(fullpath): # avoids aliases + continue + + module = os.path.splitext(module)[0] # removes the extension + self.module_list.append(module) + + + def get_module_list_text(self): + tty_size = 0 + if os.isatty(0): + tty_size = struct.unpack('HHHH', + fcntl.ioctl(0, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0)))[1] + columns = max(60, tty_size) + displace = max(len(x) for x in self.module_list) + linelimit = columns - displace - 5 + text = [] + deprecated = [] + for module in sorted(set(self.module_list)): + + if module in module_docs.BLACKLIST_MODULES: + continue + + filename = module_loader.find_plugin(module) + + if filename is None: + continue + if filename.endswith(".ps1"): + continue + if os.path.isdir(filename): + continue + + try: + doc, plainexamples, returndocs = module_docs.get_docstring(filename) + desc = self.tty_ify(doc.get('short_description', '?')).strip() + if len(desc) > linelimit: + desc = desc[:linelimit] + '...' + + if module.startswith('_'): # Handle deprecated + deprecated.append("%-*s %-*.*s" % (displace, module[1:], linelimit, len(desc), desc)) + else: + text.append("%-*s %-*.*s" % (displace, module, linelimit, len(desc), desc)) + except: + traceback.print_exc() + sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module) + + if len(deprecated) > 0: + text.append("\nDEPRECATED:") + text.extend(deprecated) + return "\n".join(text) + + @classmethod + def tty_ify(self, text): + + t = self._ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word' + t = self._BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word* + t = self._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word] + t = self._URL.sub(r"\1", t) # U(word) => word + t = self._CONST.sub("`" + r"\1" + "'", t) # C(word) => `word' + + return t diff --git a/v2/ansible/utils/module_docs.py b/v2/ansible/utils/module_docs.py new file mode 100644 index 0000000000..632b4a00c2 --- /dev/null +++ b/v2/ansible/utils/module_docs.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# (c) 2012, Jan-Piet Mens <jpmens () gmail.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import sys +import ast +import yaml +import traceback + +from ansible.plugins import fragment_loader + +# modules that are ok that they do not have documentation strings +BLACKLIST_MODULES = [ + 'async_wrapper', 'accelerate', 'async_status' +] + +def get_docstring(filename, verbose=False): + """ + Search for assignment of the DOCUMENTATION and EXAMPLES variables + in the given file. + Parse DOCUMENTATION from YAML and return the YAML doc or None + together with EXAMPLES, as plain text. + + DOCUMENTATION can be extended using documentation fragments + loaded by the PluginLoader from the module_docs_fragments + directory. + """ + + doc = None + plainexamples = None + returndocs = None + + try: + # Thank you, Habbie, for this bit of code :-) + M = ast.parse(''.join(open(filename))) + for child in M.body: + if isinstance(child, ast.Assign): + if 'DOCUMENTATION' in (t.id for t in child.targets): + doc = yaml.safe_load(child.value.s) + fragment_slug = doc.get('extends_documentation_fragment', + 'doesnotexist').lower() + + # Allow the module to specify a var other than DOCUMENTATION + # to pull the fragment from, using dot notation as a separator + if '.' in fragment_slug: + fragment_name, fragment_var = fragment_slug.split('.', 1) + fragment_var = fragment_var.upper() + else: + fragment_name, fragment_var = fragment_slug, 'DOCUMENTATION' + + + if fragment_slug != 'doesnotexist': + fragment_class = fragment_loader.get(fragment_name) + assert fragment_class is not None + + fragment_yaml = getattr(fragment_class, fragment_var, '{}') + fragment = yaml.safe_load(fragment_yaml) + + if fragment.has_key('notes'): + notes = fragment.pop('notes') + if notes: + if not doc.has_key('notes'): + doc['notes'] = [] + doc['notes'].extend(notes) + + if 'options' not in fragment.keys(): + raise Exception("missing options in fragment, possibly misformatted?") + + for key, value in fragment.items(): + if not doc.has_key(key): + doc[key] = value + else: + doc[key].update(value) + + if 'EXAMPLES' in (t.id for t in child.targets): + plainexamples = child.value.s[1:] # Skip first empty line + + if 'RETURN' in (t.id for t in child.targets): + returndocs = child.value.s[1:] + except: + traceback.print_exc() # temp + if verbose == True: + traceback.print_exc() + print "unable to parse %s" % filename + return doc, plainexamples, returndocs + diff --git a/v2/ansible/utils/module_docs_fragments b/v2/ansible/utils/module_docs_fragments new file mode 120000 index 0000000000..83aef9ec19 --- /dev/null +++ b/v2/ansible/utils/module_docs_fragments @@ -0,0 +1 @@ +../../../lib/ansible/utils/module_docs_fragments
\ No newline at end of file |