diff --git a/hacking/module_formatter.py b/hacking/module_formatter.py index 4f132aa0cf9..8dc103ffe16 100755 --- a/hacking/module_formatter.py +++ b/hacking/module_formatter.py @@ -37,7 +37,7 @@ from six import iteritems from ansible.errors import AnsibleError from ansible.module_utils._text import to_bytes -from ansible.utils import module_docs +from ansible.utils import plugin_docs ##################################################################################### # constants and paths @@ -158,7 +158,7 @@ def list_modules(module_dir, depth=0): category = category[new_cat] module = os.path.splitext(os.path.basename(module_path))[0] - if module in module_docs.BLACKLIST_MODULES: + if module in plugin_docs.BLACKLIST['MODULE']: # Do not list blacklisted modules continue if module.startswith("_") and os.path.islink(module_path): @@ -254,7 +254,7 @@ def process_module(module, options, env, template, outputname, module_map, alias print("rendering: %s" % module) # use ansible core library to parse out doc metadata YAML and plaintext examples - doc, examples, returndocs, metadata = module_docs.get_docstring(fname, verbose=options.verbose) + doc, examples, returndocs, metadata = plugin_docs.get_docstring(fname, verbose=options.verbose) # crash if module is missing documentation and not explicitly hidden from docs index if doc is None: diff --git a/lib/ansible/cli/console.py b/lib/ansible/cli/console.py index 439c040d01f..eb867606867 100644 --- a/lib/ansible/cli/console.py +++ b/lib/ansible/cli/console.py @@ -46,7 +46,7 @@ from ansible.parsing.dataloader import DataLoader from ansible.parsing.splitter import parse_kv from ansible.playbook.play import Play from ansible.plugins import module_loader -from ansible.utils import module_docs +from ansible.utils import plugin_docs from ansible.utils.color import stringc from ansible.vars import VariableManager @@ -356,7 +356,7 @@ class ConsoleCLI(CLI, cmd.Cmd): if module_name in self.modules: in_path = module_loader.find_plugin(module_name) if in_path: - oc, a, _, _ = module_docs.get_docstring(in_path) + oc, a, _, _ = plugin_docs.get_docstring(in_path) if oc: display.display(oc['short_description']) display.display('Parameters:') @@ -388,7 +388,7 @@ class ConsoleCLI(CLI, cmd.Cmd): def module_args(self, module_name): in_path = module_loader.find_plugin(module_name) - oc, a, _, _ = module_docs.get_docstring(in_path) + oc, a, _, _ = plugin_docs.get_docstring(in_path) return list(oc['options'].keys()) def run(self): diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index 79af6b97368..42ff62488b4 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -23,14 +23,15 @@ import datetime import os import traceback import textwrap +import yaml from ansible.compat.six import iteritems, string_types from ansible import constants as C from ansible.errors import AnsibleError, AnsibleOptionsError -from ansible.plugins import module_loader, action_loader +from ansible.plugins import module_loader, action_loader, lookup_loader, callback_loader, cache_loader, connection_loader, strategy_loader from ansible.cli import CLI -from ansible.utils import module_docs +from ansible.utils import plugin_docs try: from __main__ import display @@ -40,27 +41,29 @@ except ImportError: class DocCLI(CLI): - """ Vault command line class """ + """ Doc command line class """ def __init__(self, args): super(DocCLI, self).__init__(args) - self.module_list = [] + self.plugin_list = set() def parse(self): self.parser = CLI.base_parser( - usage='usage: %prog [options] [module...]', - epilog='Show Ansible module documentation', + usage='usage: %prog [options] [plugin ...]', + epilog='Show Ansible plugin documentation', module_opts=True, ) self.parser.add_option("-l", "--list", action="store_true", default=False, dest='list_dir', - help='List available modules') + help='List available plugins') 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("-a", "--all", action="store_true", default=False, dest='all_modules', - help='Show documentation for all modules') + help='Show playbook snippet for specified plugin(s)') + self.parser.add_option("-a", "--all", action="store_true", default=False, dest='all_plugins', + help='Show documentation for all plugins') + self.parser.add_option("-t", "--type", action="store", default='module', dest='type', type='choice', + help='Choose which plugin type', choices=['module','cache', 'connection', 'callback', 'lookup', 'strategy']) super(DocCLI, self).parse() @@ -70,76 +73,85 @@ class DocCLI(CLI): super(DocCLI, self).run() + plugin_type = self.options.type + + # choose plugin type + if plugin_type == 'cache': + loader = cache_loader + elif plugin_type == 'callback': + loader = callback_loader + elif plugin_type == 'connection': + loader = connection_loader + elif plugin_type == 'lookup': + loader = lookup_loader + elif plugin_type == 'strategy': + loader = strategy_loader + else: + loader = module_loader + + # add to plugin path from command line if self.options.module_path is not None: for i in self.options.module_path.split(os.pathsep): - module_loader.add_directory(i) + loader.add_directory(i) - # list modules + # list plugins for type if self.options.list_dir: - paths = module_loader._get_paths() + paths = loader._get_paths() for path in paths: - self.find_modules(path) + self.find_plugins(path, plugin_type) - self.pager(self.get_module_list_text()) + self.pager(self.get_plugin_list_text(loader)) return 0 - # process all modules - if self.options.all_modules: - paths = module_loader._get_paths() + # process all plugins of type + if self.options.all_plugins: + paths = loader._get_paths() for path in paths: - self.find_modules(path) - self.args = sorted(set(self.module_list) - module_docs.BLACKLIST_MODULES) + self.find_plugins(path, plugin_type) if len(self.args) == 0: raise AnsibleOptionsError("Incorrect options passed") - # process command line module list + # process command line list text = '' - for module in self.args: + for plugin in self.args: try: - # if the module lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs - filename = module_loader.find_plugin(module, mod_type='.py', ignore_deprecated=True) + # if the plugin lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs + filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True) if filename is None: - display.warning("module %s not found in %s\n" % (module, DocCLI.print_paths(module_loader))) + display.warning("%s %s not found in %s\n" % (plugin_type, plugin, DocCLI.print_paths(loader))) continue if any(filename.endswith(x) for x in C.BLACKLIST_EXTS): continue try: - doc, plainexamples, returndocs, metadata = module_docs.get_docstring(filename, verbose=(self.options.verbosity > 0)) + doc, plainexamples, returndocs, metadata = plugin_docs.get_docstring(filename, verbose=(self.options.verbosity > 0)) except: display.vvv(traceback.format_exc()) - display.error("module %s has a documentation error formatting or is missing documentation\nTo see exact traceback use -vvv" % module) + display.error("%s %s has a documentation error formatting or is missing documentation." % (plugin_type, plugin)) continue if doc is not None: - # is there corresponding action plugin? - if module in action_loader: - doc['action'] = True - else: - doc['action'] = False - - all_keys = [] - for (k,v) in iteritems(doc['options']): - 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') + # assign from other sections doc['plainexamples'] = plainexamples doc['returndocs'] = returndocs doc['metadata'] = metadata - if 'metadata_version' in doc['metadata']: - del doc['metadata']['metadata_version'] - if 'version' in doc['metadata']: - del doc['metadata']['metadata_version'] - if self.options.show_snippet: + # generate extra data + if plugin_type == 'module': + # is there corresponding action plugin? + if plugin in action_loader: + doc['action'] = True + else: + doc['action'] = False + doc['filename'] = filename + doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d') + doc['docuri'] = doc[plugin_type].replace('_', '-') + + if self.options.show_snippet and plugin_type == 'module': text += self.get_snippet_text(doc) else: text += self.get_man_text(doc) @@ -149,47 +161,56 @@ class DocCLI(CLI): raise AnsibleError("Parsing produced an empty object.") except Exception as e: display.vvv(traceback.format_exc()) - raise AnsibleError("module %s missing documentation (or could not parse documentation): %s\n" % (module, str(e))) + raise AnsibleError("%s %s missing documentation (or could not parse documentation): %s\n" % (plugin_type, plugin, str(e))) if text: self.pager(text) return 0 - def find_modules(self, path): - for module in os.listdir(path): - full_path = '/'.join([path, module]) + def find_plugins(self, path, ptype): - if module.startswith('.'): + display.vvvv("Searching %s for plugins" % path) + + if not os.path.exists(path): + display.vvvv("%s does not exist" % path) + return + + bkey = ptype.upper() + for plugin in os.listdir(path): + display.vvvv("Found %s" % plugin) + full_path = '/'.join([path, plugin]) + + if plugin.startswith('.'): continue elif os.path.isdir(full_path): continue - elif any(module.endswith(x) for x in C.BLACKLIST_EXTS): + elif any(plugin.endswith(x) for x in C.BLACKLIST_EXTS): continue - elif module.startswith('__'): + elif plugin.startswith('__'): continue - elif module in C.IGNORE_FILES: + elif plugin in C.IGNORE_FILES: continue - elif module.startswith('_'): + elif plugin .startswith('_'): if os.path.islink(full_path): # avoids aliases continue - module = os.path.splitext(module)[0] # removes the extension - module = module.lstrip('_') # remove underscore from deprecated modules - self.module_list.append(module) + plugin = os.path.splitext(plugin)[0] # removes the extension + plugin = plugin.lstrip('_') # remove underscore from deprecated plugins - def get_module_list_text(self): + if plugin not in plugin_docs.BLACKLIST.get(bkey, ()): + self.plugin_list.add(plugin) + display.vvvv("Added %s" % plugin) + + def get_plugin_list_text(self, loader): columns = display.columns - displace = max(len(x) for x in self.module_list) + displace = max(len(x) for x in self.plugin_list) linelimit = columns - displace - 5 text = [] deprecated = [] - for module in sorted(set(self.module_list)): - - if module in module_docs.BLACKLIST_MODULES: - continue + for plugin in sorted(self.plugin_list): # if the module lives in a non-python file (eg, win_X.ps1), require the corresponding python file for docs - filename = module_loader.find_plugin(module, mod_type='.py', ignore_deprecated=True) + filename = loader.find_plugin(plugin, mod_type='.py', ignore_deprecated=True) if filename is None: continue @@ -199,17 +220,23 @@ class DocCLI(CLI): continue try: - doc, plainexamples, returndocs, metadata = 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)) + doc, plainexamples, returndocs, metadata = plugin_docs.get_docstring(filename) except: - raise AnsibleError("module %s has a documentation error formatting or is missing documentation\n" % module) + display.warning("%s has a documentation formatting error" % plugin) + + if not doc: + desc = 'UNDOCUMENTED' + display.warning("%s parsing did not produce documentation." % plugin) + else: + desc = self.tty_ify(doc.get('short_description', '?')).strip() + + if len(desc) > linelimit: + desc = desc[:linelimit] + '...' + + if plugin.startswith('_'): # Handle deprecated + deprecated.append("%-*s %-*.*s" % (displace, plugin[1:], linelimit, len(desc), desc)) + else: + text.append("%-*s %-*.*s" % (displace, plugin, linelimit, len(desc), desc)) if len(deprecated) > 0: text.append("\nDEPRECATED:") @@ -253,34 +280,10 @@ class DocCLI(CLI): return "\n".join(text) - def get_man_text(self, doc): + def add_fields(self, text, fields, limit, opt_indent): - opt_indent=" " - text = [] - text.append("> %s (%s)\n" % (doc['module'].upper(), doc['filename'])) - pad = display.columns * 0.20 - limit = max(display.columns - int(pad), 70) - - if isinstance(doc['description'], list): - desc = " ".join(doc['description']) - else: - desc = doc['description'] - - text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), limit, initial_indent=" ", subsequent_indent=" ")) - - # FUTURE: move deprecation to metadata-only - - if 'deprecated' in doc and doc['deprecated'] is not None and len(doc['deprecated']) > 0: - text.append("DEPRECATED: \n%s\n" % doc['deprecated']) - - if 'action' in doc and doc['action']: - text.append(" * note: %s\n" % "This module has a corresponding action plugin.") - - 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] + for o in sorted(fields): + opt = fields[o] required = opt.get('required', False) if not isinstance(required, bool): @@ -306,6 +309,45 @@ class DocCLI(CLI): default = "[Default: " + str(opt.get('default', '(null)')) + "]" text.append(textwrap.fill(CLI.tty_ify(choices + default), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + for conf in ('config', 'env_vars', 'host_vars'): + if conf in opt: + text.append(textwrap.fill(CLI.tty_ify("%s: " % conf), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + for entry in opt[conf]: + if isinstance(entry, dict): + pre = " -" + for key in entry: + text.append(textwrap.fill(CLI.tty_ify("%s %s: %s" % (pre, key, entry[key])), + limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + pre = " " + else: + text.append(textwrap.fill(CLI.tty_ify(" - %s" % entry), limit, initial_indent=opt_indent, subsequent_indent=opt_indent)) + + def get_man_text(self, doc): + + opt_indent=" " + text = [] + text.append("> %s (%s)\n" % (doc[self.options.type].upper(), doc['filename'])) + pad = display.columns * 0.20 + limit = max(display.columns - int(pad), 70) + + if isinstance(doc['description'], list): + desc = " ".join(doc['description']) + else: + desc = doc['description'] + + text.append("%s\n" % textwrap.fill(CLI.tty_ify(desc), limit, initial_indent=" ", subsequent_indent=" ")) + + if 'deprecated' in doc and doc['deprecated'] is not None and len(doc['deprecated']) > 0: + text.append("DEPRECATED: \n%s\n" % doc['deprecated']) + + if 'action' in doc and doc['action']: + text.append(" * note: %s\n" % "This module has a corresponding action plugin.") + + if 'options' in doc and doc['options']: + text.append("Options (= is mandatory):\n") + self.add_fields(text, doc['options'], limit, opt_indent) + text.append('') + if 'notes' in doc and doc['notes'] and len(doc['notes']) > 0: text.append("Notes:") for note in doc['notes']: @@ -321,11 +363,18 @@ class DocCLI(CLI): text.append("%s\n" % (ex['code'])) if 'plainexamples' in doc and doc['plainexamples'] is not None: - text.append("EXAMPLES:") - text.append(doc['plainexamples']) + text.append("EXAMPLES:\n") + if isinstance(doc['plainexamples'], string_types): + text.append(doc['plainexamples']) + else: + text.append(yaml.dump(doc['plainexamples'], indent=2, default_flow_style=False)) + if 'returndocs' in doc and doc['returndocs'] is not None: - text.append("RETURN VALUES:") - text.append(doc['returndocs']) + text.append("RETURN VALUES:\n") + if isinstance(doc['returndocs'], string_types): + text.append(doc['returndocs']) + else: + text.append(yaml.dump(doc['returndocs'], indent=2, default_flow_style=False)) text.append('') maintainers = set() @@ -344,7 +393,7 @@ class DocCLI(CLI): text.append('MAINTAINERS: ' + ', '.join(maintainers)) text.append('') - if doc['metadata'] and isinstance(doc['metadata'], dict): + if 'metadata' in doc and doc['metadata']: text.append("METADATA:") for k in doc['metadata']: if isinstance(k, list): @@ -352,5 +401,4 @@ class DocCLI(CLI): else: text.append("\t%s: %s" % (k.capitalize(), doc['metadata'][k])) text.append('') - return "\n".join(text) diff --git a/lib/ansible/plugins/cache/jsonfile.py b/lib/ansible/plugins/cache/jsonfile.py index a82d7e9470a..010a026f1e5 100644 --- a/lib/ansible/plugins/cache/jsonfile.py +++ b/lib/ansible/plugins/cache/jsonfile.py @@ -14,7 +14,15 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - +''' +DOCUMENTATION: + cache: jsonfile + short_description: File backed, JSON formated. + description: + - File backed cache that uses JSON as a format, the files are per host. + version_added: "1.9" + author: Brian Coca (@bcoca) +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/cache/memory.py b/lib/ansible/plugins/cache/memory.py index ad58a974839..497af9ce5e6 100644 --- a/lib/ansible/plugins/cache/memory.py +++ b/lib/ansible/plugins/cache/memory.py @@ -14,6 +14,17 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +''' +DOCUMENTATION: + cache: memory + short_description: RAM backed, non persistent + description: + - RAM backed cache that is not persistent. + version_added: historical + author: core team (@ansible-core) +''' + + from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/cache/pickle.py b/lib/ansible/plugins/cache/pickle.py index a8018ee1961..bf61071b0fb 100644 --- a/lib/ansible/plugins/cache/pickle.py +++ b/lib/ansible/plugins/cache/pickle.py @@ -14,6 +14,15 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +''' +DOCUMENTATION: + cache: yaml + short_description: File backed, using Python's pickle. + description: + - File backed cache that uses Python's pickle serialization as a format, the files are per host. + version_added: "2.3" + author: Brian Coca (@bcoca) +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) diff --git a/lib/ansible/plugins/cache/yaml.py b/lib/ansible/plugins/cache/yaml.py index e02a652b882..bad7f593646 100644 --- a/lib/ansible/plugins/cache/yaml.py +++ b/lib/ansible/plugins/cache/yaml.py @@ -14,6 +14,15 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +''' +DOCUMENTATION: + cache: yaml + short_description: File backed, YAML formated. + description: + - File backed cache that uses YAML as a format, the files are per host. + version_added: "2.3" + author: Brian Coca (@bcoca) +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) diff --git a/lib/ansible/plugins/callback/default.py b/lib/ansible/plugins/callback/default.py index 64079caf711..d5bd68d9b33 100644 --- a/lib/ansible/plugins/callback/default.py +++ b/lib/ansible/plugins/callback/default.py @@ -14,7 +14,14 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - +''' +DOCUMENTATION: + callback: default + short_description: default Ansbile screen output + version_added: historical + description: + - This is the default output callback for ansible-playbook. +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/connection/local.py b/lib/ansible/plugins/connection/local.py index fa49a81c9ef..481fce98664 100644 --- a/lib/ansible/plugins/connection/local.py +++ b/lib/ansible/plugins/connection/local.py @@ -15,6 +15,18 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +''' +DOCUMENTATION: + connection: local + short_description: execute on controller + description: + - This connection plugin allows ansible to execute tasks on the Ansible 'controller' instead of on a remote host. + author: ansible (@core) + version_added: historical + notes: + - The remote user is ignored, the user with which the ansible CLI was executed is used instead. +''' + from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/connection/ssh.py b/lib/ansible/plugins/connection/ssh.py index 6fb2f412d84..d8f2f9463a9 100644 --- a/lib/ansible/plugins/connection/ssh.py +++ b/lib/ansible/plugins/connection/ssh.py @@ -17,6 +17,84 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +''' +DOCUMENTATION: + connection: ssh + short_description: connect via ssh client binary + description: + - This connection plugin allows ansible to communicate to the target machines via normal ssh command line. + author: ansible (@core) + version_added: historical + options: + _host: + description: Hostname/ip to connect to. + default: inventory_hostname + host_vars: + - ansible_host + - ansible_ssh_host + _host_key_checking: + type: bool + description: Determines if ssh should check host keys + config: + - section: defaults + key: 'host_key_checking' + env_vars: + - ANSIBLE_HOST_KEY_CHECKING + _password: + description: Authentication password for the C(remote_user). Can be supplied as CLI option. + host_vars: + - ansible_password + - ansible_ssh_pass + _ssh_args: + description: Arguments to pass to all ssh cli tools + default: '-C -o ControlMaster=auto -o ControlPersist=60s' + config: + - section: 'ssh_connection' + key: 'ssh_args' + env_vars: + - ANSIBLE_SSH_ARGS + _ssh_common_args: + description: Common extra args for ssh CLI tools + host_vars: + - ansible_ssh_common_args + _scp_extra_args: + description: Extra exclusive to the 'scp' CLI + host_vars: + - ansible_scp_extra_args + _sftp_extra_args: + description: Extra exclusive to the 'sftp' CLI + host_vars: + - ansible_sftp_extra_args + _ssh_extra_args: + description: Extra exclusive to the 'ssh' CLI + host_vars: + - ansible_ssh_extra_args + port: + description: Remote port to connect to. + type: int + config: + - section: defaults + key: remote_port + default: 22 + env_vars: + - ANSIBLE_REMOTE_PORT + host_vars: + - ansible_port + - ansible_ssh_port + remote_user: + description: + - User name with which to login to the remote server, normally set by the remote_user keyword. + - If no user is supplied, Ansible will let the ssh client binary choose the user as it normally + config: + - section: defaults + key: remote_user + env_vars: + - ANSIBLE_REMOTE_USER + host_vars: + - ansible_user + - ansible_ssh_user +''' + from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/lookup/cartesian.py b/lib/ansible/plugins/lookup/cartesian.py index 75d3a0735a8..1fca635d7ac 100644 --- a/lib/ansible/plugins/lookup/cartesian.py +++ b/lib/ansible/plugins/lookup/cartesian.py @@ -14,6 +14,34 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +""" +DOCUMENTATION: + lookup: cartesian + version_added: "2.1" + short_description: returns the cartesian product of lists + description: + - Takes the input lists and returns a list that represents the product of the input lists. + options: + _raw: + description: + - a set of lists + required: True +EXAMPLES: + + - name: outputs the cartesian product of the supplied lists + debug: msg="{{item}}" + with_cartesian: + - "{{list1}}" + - "{{list2}}" + - name: used as lookup changes [1, 2, 3], [a, b] into [1, a], [1, b], [2, a], [2, b], [3, a], [3, b] + debug: msg="{{ [1,2,3]|lookup('cartesian', [a, b])}}" + +RETURN: + _list: + description: + - list of lists composed of elements of the input lists + type: lists +""" from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/lookup/etcd.py b/lib/ansible/plugins/lookup/etcd.py index 25a71aa2ac2..a7fb4670928 100644 --- a/lib/ansible/plugins/lookup/etcd.py +++ b/lib/ansible/plugins/lookup/etcd.py @@ -14,6 +14,43 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +''' +DOCUMENTATION: + author: + - Jan-Piet Mens (@jpmens) + lookup: etcd + version_added: "2.1" + short_description: get info from etcd server + description: + - Retrieves data from an etcd server + options: + _raw: + description: + - the list of keys to lookup on the etcd server + type: string + required: True + _etcd_url: + description: + - Environment variable with the url for the etcd server + default: 'http://127.0.0.1:4001' + env_vars: + - ANSIBLE_ETCD_URL + _etcd_version: + description: + - Environment variable with the etcd protocol version + default: 'v1' + env_vars: + - ANSIBLE_ETCD_VERSION +EXAMPLES: + - name: "a value from a locally running etcd" + debug: msg={{ lookup('etcd', 'foo') }} +RETURN: + _list: + description: + - list of values associated with input keys + type: strings +''' + from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/strategy/debug.py b/lib/ansible/plugins/strategy/debug.py index a9159dc1b7a..b37e9ada492 100644 --- a/lib/ansible/plugins/strategy/debug.py +++ b/lib/ansible/plugins/strategy/debug.py @@ -1,3 +1,26 @@ +# 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 . +''' +DOCUMENTATION: + strategy: debug + short_description: Executes tasks in interactive debug session. + description: + - Task execution is 'linear' but controlled by an interactive debug session. + version_added: "2.1" + author: Kishin Yagami +''' from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/strategy/free.py b/lib/ansible/plugins/strategy/free.py index e63148b8b5c..6796d3c884c 100644 --- a/lib/ansible/plugins/strategy/free.py +++ b/lib/ansible/plugins/strategy/free.py @@ -14,7 +14,17 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - +''' +DOCUMENTATION: + strategy: free + short_description: Executes tasks on each host independently + description: + - Task execution is as fast as possible per host in batch as defined by C(serial) (default all). + Ansible will not wait for other hosts to finish the current task before queuing the next task for a host that has finished. + Once a host is done with the play, it opens it's slot to a new host that was waiting to start. + version_added: "2.0" + author: Ansible Core Team +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/plugins/strategy/linear.py b/lib/ansible/plugins/strategy/linear.py index f18962c7667..1c26ea312ea 100644 --- a/lib/ansible/plugins/strategy/linear.py +++ b/lib/ansible/plugins/strategy/linear.py @@ -14,7 +14,19 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . - +''' +DOCUMENTATION: + strategy: linear + short_description: Executes tasks in a linear fashion + description: + - Task execution is in lockstep per host batch as defined by C(serial) (default all). + Up to the fork limit of hosts will execute each task at the same time and then + the next series of hosts until the batch is done, before going on to the next task. + version_added: "2.0" + notes: + - This was the default Ansible behaviour before 'strategy plugins' were introduces in 2.0. + author: Ansible Core Team +''' # Make coding more python3-ish from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/lib/ansible/utils/module_docs.py b/lib/ansible/utils/module_docs.py deleted file mode 100644 index c75a3607c01..00000000000 --- a/lib/ansible/utils/module_docs.py +++ /dev/null @@ -1,147 +0,0 @@ -# (c) 2012, Jan-Piet Mens -# -# 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 . -# - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import sys -import ast -import traceback - -from collections import MutableMapping, MutableSet, MutableSequence - -from ansible.compat.six import string_types -from ansible.parsing.yaml.loader import AnsibleLoader -from ansible.plugins import fragment_loader - -try: - from __main__ import display -except ImportError: - from ansible.utils.display import Display - display = Display() - -# modules that are ok that they do not have documentation strings -BLACKLIST_MODULES = frozenset(( - 'async_wrapper', -)) - -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 - - # ensure metadata defaults - metadata = {'metadata_version': '1.0', - 'status': ['preview'], - 'supported_by': 'community'} - - 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): - for t in child.targets: - try: - theid = t.id - except AttributeError as e: - # skip errors can happen when trying to use the normal code - display.warning("Failed to assign id for %s on %s, skipping" % (t, filename)) - continue - - if 'DOCUMENTATION' == theid: - doc = AnsibleLoader(child.value.s, file_name=filename).get_single_data() - fragments = doc.get('extends_documentation_fragment', []) - - if isinstance(fragments, string_types): - fragments = [ fragments ] - - # Allow the module to specify a var other than DOCUMENTATION - # to pull the fragment from, using dot notation as a separator - for fragment_slug in fragments: - fragment_slug = fragment_slug.lower() - 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' - - fragment_class = fragment_loader.get(fragment_name) - assert fragment_class is not None - - fragment_yaml = getattr(fragment_class, fragment_var, '{}') - fragment = AnsibleLoader(fragment_yaml, file_name=filename).get_single_data() - - if 'notes' in fragment: - notes = fragment.pop('notes') - if notes: - if 'notes' not in doc: - doc['notes'] = [] - doc['notes'].extend(notes) - - if 'options' not in fragment: - raise Exception("missing options in fragment (%s), possibly misformatted?: %s" % (fragment_name, filename)) - - for key, value in fragment.items(): - if key not in doc: - doc[key] = value - else: - if isinstance(doc[key], MutableMapping): - doc[key].update(value) - elif isinstance(doc[key], MutableSet): - doc[key].add(value) - elif isinstance(doc[key], MutableSequence): - doc[key] = sorted(frozenset(doc[key] + value)) - else: - raise Exception("Attempt to extend a documentation fragement (%s) of unknown type: %s" % (fragment_name, filename)) - - elif 'EXAMPLES' == theid: - plainexamples = child.value.s[1:] # Skip first empty line - - elif 'RETURN' == theid: - returndocs = child.value.s[1:] - - elif 'ANSIBLE_METADATA' == theid: - metadata = ast.literal_eval(child.value) - if not isinstance(metadata, MutableMapping): - # try yaml loading - metadata = AnsibleLoader(metadata, file_name=filename).get_single_data() - - if not isinstance(metadata, MutableMapping): - display.warning("Invalid metadata detected in %s, using defaults" % filename) - metadata = {'status': ['preview'], 'supported_by': 'community', 'metadata_version': '1.0'} - - except: - display.error("unable to parse %s" % filename) - if verbose is True: - display.display("unable to parse %s" % filename) - raise - - return doc, plainexamples, returndocs, metadata diff --git a/lib/ansible/utils/plugin_docs.py b/lib/ansible/utils/plugin_docs.py new file mode 100644 index 00000000000..9a153253ec9 --- /dev/null +++ b/lib/ansible/utils/plugin_docs.py @@ -0,0 +1,170 @@ +# (c) 2012, Jan-Piet Mens +# +# 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 . +# + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ast +import yaml + +from collections import MutableMapping, MutableSet, MutableSequence + +from ansible.compat.six import string_types +from ansible.parsing.yaml.loader import AnsibleLoader +from ansible.plugins import fragment_loader + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +# modules that are ok that they do not have documentation strings +BLACKLIST = { + 'MODULE': frozenset(( 'async_wrapper',)), + 'CACHE': frozenset(( 'base',)), +} + + +def add_fragments(doc, filename): + + fragments = doc.get('extends_documentation_fragment', []) + + if isinstance(fragments, string_types): + fragments = [ fragments ] + + # Allow the module to specify a var other than DOCUMENTATION + # to pull the fragment from, using dot notation as a separator + for fragment_slug in fragments: + fragment_slug = fragment_slug.lower() + 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' + + fragment_class = fragment_loader.get(fragment_name) + assert fragment_class is not None + + fragment_yaml = getattr(fragment_class, fragment_var, '{}') + fragment = AnsibleLoader(fragment_yaml, file_name=filename).get_single_data() + + if 'notes' in fragment: + notes = fragment.pop('notes') + if notes: + if 'notes' not in doc: + doc['notes'] = [] + doc['notes'].extend(notes) + + if 'options' not in fragment: + raise Exception("missing options in fragment (%s), possibly misformatted?: %s" % (fragment_name, filename)) + + for key, value in fragment.items(): + if key not in doc: + doc[key] = value + else: + if isinstance(doc[key], MutableMapping): + doc[key].update(value) + elif isinstance(doc[key], MutableSet): + doc[key].add(value) + elif isinstance(doc[key], MutableSequence): + doc[key] = sorted(frozenset(doc[key] + value)) + else: + raise Exception("Attempt to extend a documentation fragement (%s) of unknown type: %s" % (fragment_name, filename)) + + +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. + """ + + data = { + 'doc': None, + 'plainexamples': None, + 'returndocs': None, + 'metadata': None + } + + string_to_vars = { + 'DOCUMENTATION': 'doc', + 'EXAMPLES': 'plainexamples', + 'RETURN': 'returndocs', + 'METADATA': 'metadata' + } + + try: + M = ast.parse(''.join(open(filename))) + try: + display.debug('Attempt first docstring is yaml docs') + docstring = yaml.load(M.body[0].value.s) + for string in string_to_vars.keys(): + if string in docstring: + data[string_to_vars[string]] = docstring[string] + display.debug('assigned :%s' % string_to_vars[string]) + except Exception as e: + display.debug('failed docstring parsing: %s' % str(e)) + + if not 'docs' in data or not data['docs']: + display.debug('Fallback to vars parsing') + for child in M.body: + if isinstance(child, ast.Assign): + for t in child.targets: + try: + theid = t.id + except AttributeError: + # skip errors can happen when trying to use the normal code + display.warning("Failed to assign id for %s on %s, skipping" % (t, filename)) + continue + + if theid in string_to_vars: + varkey = string_to_vars[theid] + if isinstance(child.value, MutableMapping): + data[varkey] = child.value + else: + if theid in ['DOCUMENTATION', 'METADATA']: + # string should be yaml + data[varkey] = AnsibleLoader(child.value.s, file_name=filename).get_single_data() + else: + # not yaml, should be a simple string + data[varkey] = child.value.s + display.debug('assigned :%s' % varkey) + + # add fragments to documentation + if data['doc']: + add_fragments(data['doc'], filename) + + # remove version + if data['metadata']: + for x in ('version', 'metadata_version'): + if x in data['metadata']: + del data['metadata'][x] + except: + display.error("unable to parse %s" % filename) + if verbose is True: + display.display("unable to parse %s" % filename) + raise + + return data['doc'], data['plainexamples'], data['returndocs'], data['metadata'] diff --git a/test/sanity/validate-modules/validate-modules b/test/sanity/validate-modules/validate-modules index f95673841d0..07e3089c57f 100755 --- a/test/sanity/validate-modules/validate-modules +++ b/test/sanity/validate-modules/validate-modules @@ -37,7 +37,7 @@ from fnmatch import fnmatch from ansible import __version__ as ansible_version from ansible.executor.module_common import REPLACER_WINDOWS -from ansible.utils.module_docs import BLACKLIST_MODULES, get_docstring +from ansible.utils.plugin_docs import BLACKLIST, get_docstring from module_args import get_argument_spec @@ -197,7 +197,7 @@ class ModuleValidator(Validator): 'shippable.yml', '.gitattributes', '.gitmodules', 'COPYING', '__init__.py', 'VERSION', 'test-docs.sh')) - BLACKLIST = BLACKLIST_FILES.union(BLACKLIST_MODULES) + BLACKLIST = BLACKLIST_FILES.union(BLACKLIST['MODULE']) PS_DOC_BLACKLIST = frozenset(( 'async_status.ps1',