b3b356480d
'_', they will be loaded after non prefixed modules are checked they can be full modules or symlinks to existing ones (alias) also updated ansible doc to ignore these, will eventually add selective display
297 lines
9.4 KiB
Python
Executable file
297 lines
9.4 KiB
Python
Executable file
#!/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 textwrap
|
|
import re
|
|
import optparse
|
|
import datetime
|
|
import subprocess
|
|
from ansible import utils
|
|
from ansible.utils import module_docs
|
|
import ansible.constants as C
|
|
from ansible.utils import version
|
|
import traceback
|
|
|
|
MODULEDIR = C.DEFAULT_MODULE_PATH
|
|
|
|
BLACKLIST_EXTS = ('.pyc', '.swp', '.bak', '~', '.rpm')
|
|
|
|
_ITALIC = re.compile(r"I\(([^)]+)\)")
|
|
_BOLD = re.compile(r"B\(([^)]+)\)")
|
|
_MODULE = re.compile(r"M\(([^)]+)\)")
|
|
_URL = re.compile(r"U\(([^)]+)\)")
|
|
_CONST = re.compile(r"C\(([^)]+)\)")
|
|
PAGER = 'less'
|
|
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 pager_print(text):
|
|
''' just print text '''
|
|
print text
|
|
|
|
def pager_pipe(text, cmd):
|
|
''' pipe text through a pager '''
|
|
if 'LESS' not in os.environ:
|
|
os.environ['LESS'] = LESS_OPTS
|
|
try:
|
|
cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
|
|
cmd.communicate(input=text)
|
|
except IOError:
|
|
pass
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
def pager(text):
|
|
''' find reasonable way to display text '''
|
|
# this is a much simpler form of what is in pydoc.py
|
|
if not sys.stdout.isatty():
|
|
pager_print(text)
|
|
elif 'PAGER' in os.environ:
|
|
if sys.platform == 'win32':
|
|
pager_print(text)
|
|
else:
|
|
pager_pipe(text, os.environ['PAGER'])
|
|
elif hasattr(os, 'system') and os.system('(less) 2> /dev/null') == 0:
|
|
pager_pipe(text, 'less')
|
|
else:
|
|
pager_print(text)
|
|
|
|
def tty_ify(text):
|
|
|
|
t = _ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word'
|
|
t = _BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word*
|
|
t = _MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
|
|
t = _URL.sub(r"\1", t) # U(word) => word
|
|
t = _CONST.sub("`" + r"\1" + "'", t) # C(word) => `word'
|
|
|
|
return t
|
|
|
|
def get_man_text(doc):
|
|
|
|
opt_indent=" "
|
|
text = []
|
|
text.append("> %s\n" % doc['module'].upper())
|
|
|
|
desc = " ".join(doc['description'])
|
|
|
|
text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=" ", subsequent_indent=" "))
|
|
|
|
if 'option_keys' in doc and len(doc['option_keys']) > 0:
|
|
text.append("Options (= is mandatory):\n")
|
|
|
|
for o in sorted(doc['option_keys']):
|
|
opt = doc['options'][o]
|
|
|
|
if opt.get('required', False):
|
|
opt_leadin = "="
|
|
else:
|
|
opt_leadin = "-"
|
|
|
|
text.append("%s %s" % (opt_leadin, o))
|
|
|
|
desc = " ".join(opt['description'])
|
|
|
|
if 'choices' in opt:
|
|
choices = ", ".join(str(i) for i in opt['choices'])
|
|
desc = desc + " (Choices: " + choices + ")"
|
|
if 'default' in opt:
|
|
default = str(opt['default'])
|
|
desc = desc + " [Default: " + default + "]"
|
|
text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent,
|
|
subsequent_indent=opt_indent))
|
|
|
|
if 'notes' in doc and len(doc['notes']) > 0:
|
|
notes = " ".join(doc['notes'])
|
|
text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent=" ",
|
|
subsequent_indent=opt_indent))
|
|
|
|
|
|
if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0:
|
|
req = ", ".join(doc['requirements'])
|
|
text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent=" ",
|
|
subsequent_indent=opt_indent))
|
|
|
|
if 'examples' in doc and len(doc['examples']) > 0:
|
|
text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's'))
|
|
for ex in doc['examples']:
|
|
text.append("%s\n" % (ex['code']))
|
|
|
|
if 'plainexamples' in doc and doc['plainexamples'] is not None:
|
|
text.append(doc['plainexamples'])
|
|
text.append('')
|
|
|
|
return "\n".join(text)
|
|
|
|
|
|
def get_snippet_text(doc):
|
|
|
|
text = []
|
|
desc = tty_ify(" ".join(doc['short_description']))
|
|
text.append("- name: %s" % (desc))
|
|
text.append(" action: %s" % (doc['module']))
|
|
|
|
for o in sorted(doc['options'].keys()):
|
|
opt = doc['options'][o]
|
|
desc = tty_ify(" ".join(opt['description']))
|
|
|
|
if opt.get('required', False):
|
|
s = o + "="
|
|
else:
|
|
s = o
|
|
|
|
text.append(" %-20s # %s" % (s, desc))
|
|
text.append('')
|
|
|
|
return "\n".join(text)
|
|
|
|
def get_module_list_text(module_list):
|
|
text = []
|
|
for module in sorted(set(module_list)):
|
|
|
|
if module in module_docs.BLACKLIST_MODULES:
|
|
continue
|
|
|
|
filename = utils.plugins.module_finder.find_plugin(module)
|
|
|
|
if filename is None:
|
|
continue
|
|
if filename.endswith(".ps1"):
|
|
continue
|
|
if os.path.isdir(filename):
|
|
continue
|
|
|
|
try:
|
|
doc, plainexamples = module_docs.get_docstring(filename)
|
|
desc = tty_ify(doc.get('short_description', '?'))
|
|
if len(desc) > 55:
|
|
desc = desc + '...'
|
|
text.append("%-20s %-60.60s" % (module, desc))
|
|
except:
|
|
traceback.print_exc()
|
|
sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
|
|
return "\n".join(text)
|
|
|
|
def main():
|
|
|
|
p = optparse.OptionParser(
|
|
version=version("%prog"),
|
|
usage='usage: %prog [options] [module...]',
|
|
description='Show Ansible module documentation',
|
|
)
|
|
|
|
p.add_option("-M", "--module-path",
|
|
action="store",
|
|
dest="module_path",
|
|
default=MODULEDIR,
|
|
help="Ansible modules/ directory")
|
|
p.add_option("-l", "--list",
|
|
action="store_true",
|
|
default=False,
|
|
dest='list_dir',
|
|
help='List available modules')
|
|
p.add_option("-s", "--snippet",
|
|
action="store_true",
|
|
default=False,
|
|
dest='show_snippet',
|
|
help='Show playbook snippet for specified module(s)')
|
|
p.add_option('-v', action='version', help='Show version number and exit')
|
|
|
|
(options, args) = p.parse_args()
|
|
|
|
if options.module_path is not None:
|
|
for i in options.module_path.split(os.pathsep):
|
|
utils.plugins.module_finder.add_directory(i)
|
|
|
|
if options.list_dir:
|
|
# list all modules
|
|
paths = utils.plugins.module_finder._get_paths()
|
|
module_list = []
|
|
deprecated_list = []
|
|
module_aliases = {}
|
|
for path in paths:
|
|
# os.system("ls -C %s" % (path))
|
|
if os.path.isdir(path):
|
|
for module in os.listdir(path):
|
|
if module.startswith('_') or any(module.endswith(x) for x in BLACKLIST_EXTS):
|
|
continue
|
|
module_list.append(module)
|
|
|
|
pager(get_module_list_text(module_list))
|
|
sys.exit()
|
|
|
|
if len(args) == 0:
|
|
p.print_help()
|
|
|
|
def print_paths(finder):
|
|
''' Returns a string suitable for printing of the search path '''
|
|
|
|
# Uses a list to get the order right
|
|
ret = []
|
|
for i in finder._get_paths():
|
|
if i not in ret:
|
|
ret.append(i)
|
|
return os.pathsep.join(ret)
|
|
|
|
text = ''
|
|
for module in args:
|
|
|
|
filename = utils.plugins.module_finder.find_plugin(module)
|
|
if filename is None:
|
|
sys.stderr.write("module %s not found in %s\n" % (module,
|
|
print_paths(utils.plugins.module_finder)))
|
|
continue
|
|
|
|
if any(filename.endswith(x) for x in BLACKLIST_EXTS):
|
|
continue
|
|
|
|
try:
|
|
doc, plainexamples = module_docs.get_docstring(filename)
|
|
except:
|
|
traceback.print_exc()
|
|
sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module)
|
|
continue
|
|
|
|
if doc is not None:
|
|
|
|
all_keys = []
|
|
for (k,v) in doc['options'].iteritems():
|
|
all_keys.append(k)
|
|
all_keys = sorted(all_keys)
|
|
doc['option_keys'] = all_keys
|
|
|
|
doc['filename'] = filename
|
|
doc['docuri'] = doc['module'].replace('_', '-')
|
|
doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d')
|
|
doc['plainexamples'] = plainexamples
|
|
|
|
if options.show_snippet:
|
|
text += get_snippet_text(doc)
|
|
else:
|
|
text += get_man_text(doc)
|
|
else:
|
|
# this typically means we couldn't even parse the docstring, not just that the YAML is busted,
|
|
# probably a quoting issue.
|
|
sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module)
|
|
pager(text)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|