diff --git a/changelogs/fragments/plugin_attributes.yml b/changelogs/fragments/plugin_attributes.yml new file mode 100644 index 00000000000..a884d02539c --- /dev/null +++ b/changelogs/fragments/plugin_attributes.yml @@ -0,0 +1,2 @@ +minor_changes: + - ansible-doc now supports 'attributes' for plugins as per proposal. diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index ca4d866ad0e..bd2896411f5 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -54,6 +54,7 @@ def jdump(text): try: display.display(json.dumps(text, cls=AnsibleJSONEncoder, sort_keys=True, indent=4)) except TypeError as e: + display.vvv(traceback.format_exc()) raise AnsibleError('We could not convert all the documentation into JSON as there was a conversion issue: %s' % to_native(e)) @@ -780,7 +781,8 @@ class DocCLI(CLI, RoleMixin): try: text = DocCLI.get_man_text(doc, collection_name, plugin_type) except Exception as e: - raise AnsibleError("Unable to retrieve documentation from '%s' due to: %s" % (plugin, to_native(e))) + display.vvv(traceback.format_exc()) + raise AnsibleError("Unable to retrieve documentation from '%s' due to: %s" % (plugin, to_native(e)), orig_exc=e) return text @@ -874,6 +876,7 @@ class DocCLI(CLI, RoleMixin): pfiles[plugin] = filename except Exception as e: + display.vvv(traceback.format_exc()) raise AnsibleError("Failed reading docs at %s: %s" % (plugin, to_native(e)), orig_exc=e) return pfiles @@ -921,9 +924,7 @@ class DocCLI(CLI, RoleMixin): @staticmethod def _dump_yaml(struct, indent): - return DocCLI.tty_ify('\n'.join([indent + line for line in - yaml.dump(struct, default_flow_style=False, - Dumper=AnsibleDumper).split('\n')])) + return DocCLI.tty_ify('\n'.join([indent + line for line in yaml.dump(struct, default_flow_style=False, Dumper=AnsibleDumper).split('\n')])) @staticmethod def add_fields(text, fields, limit, opt_indent, return_values=False, base_indent=''): @@ -1051,6 +1052,11 @@ class DocCLI(CLI, RoleMixin): DocCLI.add_fields(text, doc.pop('options'), limit, opt_indent) text.append('') + if doc.get('attributes'): + text.append("ATTRIBUTES:\n") + text.append(DocCLI._dump_yaml(doc.pop('attributes'), opt_indent)) + text.append('') + # generic elements we will handle identically for k in ('author',): if k not in doc: @@ -1115,6 +1121,11 @@ class DocCLI(CLI, RoleMixin): DocCLI.add_fields(text, doc.pop('options'), limit, opt_indent) text.append('') + if doc.get('attributes', False): + text.append("ATTRIBUTES:\n") + text.append(DocCLI._dump_yaml(doc.pop('attributes'), opt_indent)) + text.append('') + if doc.get('notes', False): text.append("NOTES:") for note in doc['notes']: diff --git a/lib/ansible/modules/copy.py b/lib/ansible/modules/copy.py index 993143f4b65..0302c86c9e9 100644 --- a/lib/ansible/modules/copy.py +++ b/lib/ansible/modules/copy.py @@ -121,9 +121,9 @@ extends_documentation_fragment: - decrypt - files - validate +- action_common_attributes notes: - The M(ansible.builtin.copy) module recursively copy facility does not scale to lots (>hundreds) of files. -- Supports C(check_mode). seealso: - module: ansible.builtin.assemble - module: ansible.builtin.fetch @@ -134,6 +134,15 @@ seealso: author: - Ansible Core Team - Michael DeHaan +attributes: + action: + support: full + check_mode: + version_added: '0.9' + support: full + diff_mode: + support: full + version_added: '0.9' ''' EXAMPLES = r''' diff --git a/lib/ansible/modules/import_playbook.py b/lib/ansible/modules/import_playbook.py index 767a8a450f3..ee599d669cc 100644 --- a/lib/ansible/modules/import_playbook.py +++ b/lib/ansible/modules/import_playbook.py @@ -22,6 +22,27 @@ options: free-form: description: - The name of the imported playbook is specified directly without any other option. +extends_documentation_fragment: +- action_common_attributes +attributes: + async: + support: none + become: + support: none + bypass_host_loop: + support: full + conditional: + support: none + connection: + support: none + delegation: + support: none + loops: + support: none + tags: + support: none + until: + support: none notes: - This is a core feature of Ansible, rather than a module, and cannot be overridden like a module. seealso: diff --git a/lib/ansible/modules/import_role.py b/lib/ansible/modules/import_role.py index 8de4f428d83..386837b1668 100644 --- a/lib/ansible/modules/import_role.py +++ b/lib/ansible/modules/import_role.py @@ -57,6 +57,27 @@ options: type: bool default: yes version_added: '2.11' +extends_documentation_fragment: +- action_common_attributes +attributes: + async: + support: none + become: + support: none + bypass_host_loop: + support: partial + conditional: + support: none + connection: + support: none + delegation: + support: none + loops: + support: none + tags: + support: none + until: + support: none notes: - Handlers are made available to the whole play. - Since Ansible 2.7 variables defined in C(vars) and C(defaults) for the role are exposed to the play at playbook parsing time. diff --git a/lib/ansible/modules/import_tasks.py b/lib/ansible/modules/import_tasks.py index 829e291a71d..8db81ab332f 100644 --- a/lib/ansible/modules/import_tasks.py +++ b/lib/ansible/modules/import_tasks.py @@ -22,6 +22,27 @@ options: - The name of the imported file is specified directly without any other option. - Most keywords, including loops and conditionals, only applied to the imported tasks, not to this statement itself. - If you need any of those to apply, use M(ansible.builtin.include_tasks) instead. +extends_documentation_fragment: +- action_common_attributes +attributes: + async: + support: none + become: + support: none + bypass_host_loop: + support: partial + conditional: + support: none + connection: + support: none + delegation: + support: none + loops: + support: none + tags: + support: none + until: + support: none notes: - This is a core feature of Ansible, rather than a module, and cannot be overridden like a module. seealso: diff --git a/lib/ansible/plugins/doc_fragments/action_common_attributes.py b/lib/ansible/plugins/doc_fragments/action_common_attributes.py new file mode 100644 index 00000000000..31113f2e865 --- /dev/null +++ b/lib/ansible/plugins/doc_fragments/action_common_attributes.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Ansible, Inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard documentation fragment + DOCUMENTATION = r''' +attributes: + action: + description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller + support: none + async: + description: Supports being used with the ``async`` keyword + support: full + become: + description: Is usable alongside become keywords + support: full + bypass_host_loop: + description: Forces a 'global' task that does not execute per host, cannot be used in non lockstep strategies + support: none + check_mode: + description: Can run in check_mode and return changed status prediction + support: none + connection: + description: Uses the target's configured connection information to execute code on it + support: full + conditional: + description: Will respect the `when` keyword per item loop or task (when no loop is present) + support: full + delegation: + description: Can be used in conjunction with delegate_to and related keywords + support: full + diff: + description: Will return details on what has changed when in diff is enabled + support: none + facts: + description: Action returns an ``ansible_facts`` dictionary that will update existing host facts + support: none + loops: + description: both ``loop`` and ``with_`` looping keywords will be honored. + support: full + proprietary: + description: Can only be run against specific proprietary OS, normally a network appliance or similar + support: none + posix: + description: Can be run against most POSIX (and GNU/Linux) OS targets + support: full + tags: + description: Tags will determine if this task considered for execution + support: full + until: + description: Usable inside until/retry loops + support: full + windows: + description: Can be run against Windows OS targets + support: none +''' diff --git a/lib/ansible/utils/plugin_docs.py b/lib/ansible/utils/plugin_docs.py index 7dca58e4050..dc1160d4853 100644 --- a/lib/ansible/utils/plugin_docs.py +++ b/lib/ansible/utils/plugin_docs.py @@ -6,13 +6,14 @@ __metaclass__ = type from ansible import constants as C from ansible.release import __version__ as ansible_version -from ansible.errors import AnsibleError, AnsibleAssertionError +from ansible.errors import AnsibleError from ansible.module_utils.six import string_types from ansible.module_utils._text import to_native from ansible.module_utils.common._collections_compat import MutableMapping, MutableSet, MutableSequence from ansible.parsing.plugin_docs import read_docstring from ansible.parsing.yaml.loader import AnsibleLoader from ansible.utils.display import Display +from ansible.utils.vars import combine_vars display = Display() @@ -179,17 +180,19 @@ def add_fragments(doc, filename, fragment_loader, is_module=False): doc['seealso'] = [] doc['seealso'].extend(seealso) - if 'options' not in fragment: - raise Exception("missing options in fragment (%s), possibly misformatted?: %s" % (fragment_name, filename)) + if 'options' not in fragment and 'attributes' not in fragment: + raise Exception("missing options or attributes in fragment (%s), possibly misformatted?: %s" % (fragment_name, filename)) # ensure options themselves are directly merged - if 'options' in doc: - try: - merge_fragment(doc['options'], fragment.pop('options')) - except Exception as e: - raise AnsibleError("%s options (%s) of unknown type: %s" % (to_native(e), fragment_name, filename)) - else: - doc['options'] = fragment.pop('options') + for doc_key in ['options', 'attributes']: + if doc_key in fragment: + if doc_key in doc: + try: + merge_fragment(doc[doc_key], fragment.pop(doc_key)) + except Exception as e: + raise AnsibleError("%s %s (%s) of unknown type: %s" % (to_native(e), doc_key, fragment_name, filename)) + else: + doc[doc_key] = fragment.pop(doc_key) # merge rest of the sections try: diff --git a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py index fc64c960491..d13831b5f6f 100644 --- a/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py +++ b/test/lib/ansible_test/_data/sanity/validate-modules/validate_modules/schema.py @@ -522,7 +522,7 @@ def doc_schema(module_name, for_collection=False, deprecated_module=False): All( Schema( doc_schema_dict, - extra=PREVENT_EXTRA + extra=ALLOW_EXTRA ), partial(version_added, error_code='module-invalid-version-added', accept_historical=not for_collection), )