summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Coca <brian.coca+git@gmail.com>2015-04-30 22:29:12 -0400
committerBrian Coca <brian.coca+git@gmail.com>2015-04-30 22:33:29 -0400
commit38d2042739dd3c2c295ecf11267ebcc07bce5bf4 (patch)
tree98989c6917fc87ad6bc1be6f3240bd3b5ef1e821
parentdf881b7f37bb53287c504f0180ad2813eaf36e03 (diff)
downloadansible-38d2042739dd3c2c295ecf11267ebcc07bce5bf4.tar.gz
v2 ansible-doc can now list modules
-rw-r--r--v2/ansible/cli/doc.py114
-rw-r--r--v2/ansible/utils/module_docs.py102
l---------v2/ansible/utils/module_docs_fragments1
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