diff --git a/lib/ansible/cli/arguments/option_helpers.py b/lib/ansible/cli/arguments/option_helpers.py index 8465f7510bb..043b7b5d489 100644 --- a/lib/ansible/cli/arguments/option_helpers.py +++ b/lib/ansible/cli/arguments/option_helpers.py @@ -221,7 +221,8 @@ def add_basedir_options(parser): """Add options for commands which can set a playbook basedir""" parser.add_argument('--playbook-dir', default=C.config.get_config_value('PLAYBOOK_DIR'), dest='basedir', action='store', help="Since this tool does not use playbooks, use this as a substitute playbook directory." - "This sets the relative path for many features including roles/ group_vars/ etc.") + "This sets the relative path for many features including roles/ group_vars/ etc.", + type=unfrack_path()) def add_check_options(parser): diff --git a/lib/ansible/cli/doc.py b/lib/ansible/cli/doc.py index 8372e330d14..e1299a9032f 100644 --- a/lib/ansible/cli/doc.py +++ b/lib/ansible/cli/doc.py @@ -9,6 +9,7 @@ import datetime import json import pkgutil import os +import os.path import re import textwrap import traceback @@ -26,12 +27,13 @@ from ansible.module_utils._text import to_native, to_text from ansible.module_utils.common._collections_compat import Container, Sequence from ansible.module_utils.common.json import AnsibleJSONEncoder from ansible.module_utils.compat import importlib -from ansible.module_utils.six import string_types +from ansible.module_utils.six import iteritems, string_types +from ansible.parsing.dataloader import DataLoader from ansible.parsing.plugin_docs import read_docstub from ansible.parsing.utils.yaml import from_yaml from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.plugins.loader import action_loader, fragment_loader -from ansible.utils.collection_loader import AnsibleCollectionConfig +from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path from ansible.utils.display import Display from ansible.utils.plugin_docs import ( @@ -44,7 +46,7 @@ from ansible.utils.plugin_docs import ( display = Display() -TARGET_OPTIONS = C.DOCUMENTABLE_PLUGINS + ('keyword',) +TARGET_OPTIONS = C.DOCUMENTABLE_PLUGINS + ('role', 'keyword',) PB_OBJECTS = ['Play', 'Role', 'Block', 'Task'] PB_LOADED = {} @@ -71,7 +73,206 @@ class PluginNotFound(Exception): pass -class DocCLI(CLI): +class RoleMixin(object): + """A mixin containing all methods relevant to role argument specification functionality. + + Note: The methods for actual display of role data are not present here. + """ + + ROLE_ARGSPEC_FILE = 'argument_specs.yml' + + def _load_argspec(self, role_name, collection_path=None, role_path=None): + if collection_path: + path = os.path.join(collection_path, 'roles', role_name, 'meta', self.ROLE_ARGSPEC_FILE) + elif role_path: + path = os.path.join(role_path, 'meta', self.ROLE_ARGSPEC_FILE) + else: + raise AnsibleError("A path is required to load argument specs for role '%s'" % role_name) + + loader = DataLoader() + return loader.load_from_file(path, cache=False, unsafe=True) + + def _find_all_normal_roles(self, role_paths, name_filters=None): + """Find all non-collection roles that have an argument spec file. + + :param role_paths: A tuple of one or more role paths. When a role with the same name + is found in multiple paths, only the first-found role is returned. + :param name_filters: A tuple of one or more role names used to filter the results. + + :returns: A set of tuples consisting of: role name, full role path + """ + found = set() + found_names = set() + for path in role_paths: + if not os.path.isdir(path): + continue + # Check each subdir for a meta/argument_specs.yml file + for entry in os.listdir(path): + role_path = os.path.join(path, entry) + full_path = os.path.join(role_path, 'meta', self.ROLE_ARGSPEC_FILE) + if os.path.exists(full_path): + if name_filters is None or entry in name_filters: + if entry not in found_names: + found.add((entry, role_path)) + found_names.add(entry) + return found + + def _find_all_collection_roles(self, name_filters=None, collection_filter=None): + """Find all collection roles with an argument spec. + + :param name_filters: A tuple of one or more role names used to filter the results. These + might be fully qualified with the collection name (e.g., community.general.roleA) + or not (e.g., roleA). + + :param collection_filter: A string containing the FQCN of a collection which will be + used to limit results. This filter will take precedence over the name_filters. + + :returns: A set of tuples consisting of: role name, collection name, collection path + """ + found = set() + b_colldirs = list_collection_dirs(coll_filter=collection_filter) + for b_path in b_colldirs: + path = to_text(b_path, errors='surrogate_or_strict') + collname = _get_collection_name_from_path(b_path) + + roles_dir = os.path.join(path, 'roles') + if os.path.exists(roles_dir): + for entry in os.listdir(roles_dir): + full_path = os.path.join(roles_dir, entry, 'meta', self.ROLE_ARGSPEC_FILE) + if os.path.exists(full_path): + if name_filters is None: + found.add((entry, collname, path)) + else: + # Name filters might contain a collection FQCN or not. + for fqcn in name_filters: + if len(fqcn.split('.')) == 3: + (ns, col, role) = fqcn.split('.') + if '.'.join([ns, col]) == collname and entry == role: + found.add((entry, collname, path)) + elif fqcn == entry: + found.add((entry, collname, path)) + return found + + def _build_summary(self, role, collection, argspec): + """Build a summary dict for a role. + + Returns a simplified role arg spec containing only the role entry points and their + short descriptions, and the role collection name (if applicable). + + :param role: The simple role name. + :param collection: The collection containing the role (None or empty string if N/A). + :param argspec: The complete role argspec data dict. + + :returns: A tuple with the FQCN role name and a summary dict. + """ + if collection: + fqcn = '.'.join([collection, role]) + else: + fqcn = role + summary = {} + summary['collection'] = collection + summary['entry_points'] = {} + for entry_point in argspec.keys(): + entry_spec = argspec[entry_point] or {} + summary['entry_points'][entry_point] = entry_spec.get('short_description', '') + return (fqcn, summary) + + def _create_role_list(self, roles_path, collection_filter=None): + """Return a dict describing the listing of all roles with arg specs. + + :param role_paths: A tuple of one or more role paths. + + :returns: A dict indexed by role name, with 'collection' and 'entry_points' keys per role. + + Example return: + + results = { + 'roleA': { + 'collection': '', + 'entry_points': { + 'main': 'Short description for main' + } + }, + 'a.b.c.roleB': { + 'collection': 'a.b.c', + 'entry_points': { + 'main': 'Short description for main', + 'alternate': 'Short description for alternate entry point' + } + 'x.y.z.roleB': { + 'collection': 'x.y.z', + 'entry_points': { + 'main': 'Short description for main', + } + }, + } + """ + if not collection_filter: + roles = self._find_all_normal_roles(roles_path) + else: + roles = [] + collroles = self._find_all_collection_roles(collection_filter=collection_filter) + + result = {} + + for role, role_path in roles: + argspec = self._load_argspec(role, role_path=role_path) + fqcn, summary = self._build_summary(role, '', argspec) + result[fqcn] = summary + + for role, collection, collection_path in collroles: + argspec = self._load_argspec(role, collection_path=collection_path) + fqcn, summary = self._build_summary(role, collection, argspec) + result[fqcn] = summary + + return result + + def _create_role_doc(self, role_names, roles_path, entry_point=None): + """ + :param role_names: A tuple of one or more role names. + :param role_paths: A tuple of one or more role paths. + :param entry_point: A role entry point name for filtering. + + :returns: A dict indexed by role name, with 'collection', 'entry_points', and 'path' keys per role. + """ + roles = self._find_all_normal_roles(roles_path, name_filters=role_names) + collroles = self._find_all_collection_roles(name_filters=role_names) + result = {} + + def build_doc(role, path, collection, argspec): + if collection: + fqcn = '.'.join([collection, role]) + else: + fqcn = role + if fqcn not in result: + result[fqcn] = {} + doc = {} + doc['path'] = path + doc['collection'] = collection + doc['entry_points'] = {} + for ep in argspec.keys(): + if entry_point is None or ep == entry_point: + entry_spec = argspec[ep] or {} + doc['entry_points'][ep] = entry_spec + + # If we didn't add any entry points (b/c of filtering), remove this entry. + if len(doc['entry_points'].keys()) == 0: + del result[fqcn] + else: + result[fqcn] = doc + + for role, role_path in roles: + argspec = self._load_argspec(role, role_path=role_path) + build_doc(role, role_path, '', argspec) + + for role, collection, collection_path in collroles: + argspec = self._load_argspec(role, collection_path=collection_path) + build_doc(role, collection_path, collection, argspec) + + return result + + +class DocCLI(CLI, RoleMixin): ''' displays information on modules installed in Ansible libraries. It displays a terse listing of plugins and their short descriptions, provides a printout of their DOCUMENTATION strings, @@ -141,6 +342,12 @@ class DocCLI(CLI): self.parser.add_argument("-j", "--json", action="store_true", default=False, dest='json_format', help='Change output into json format.') + # role-specific options + self.parser.add_argument("-r", "--roles-path", dest='roles_path', default=C.DEFAULT_ROLES_PATH, + type=opt_help.unfrack_path(pathsep=True), + action=opt_help.PrependListAction, + help='The path to the directory containing your roles.') + exclusive = self.parser.add_mutually_exclusive_group() exclusive.add_argument("-F", "--list_files", action="store_true", default=False, dest="list_files", help='Show plugin names and their source files without summaries (implies --list). %s' % coll_filter) @@ -150,6 +357,8 @@ class DocCLI(CLI): help='Show playbook snippet for specified plugin(s)') exclusive.add_argument("--metadata-dump", action="store_true", default=False, dest='dump', help='**For internal testing only** Dump json metadata for all plugins.') + exclusive.add_argument("-e", "--entry-point", dest="entry_point", + help="Select the entry point for role(s).") def post_process_args(self, options): options = super(DocCLI, self).post_process_args(options) @@ -192,6 +401,48 @@ class DocCLI(CLI): # display results DocCLI.pager("\n".join(text)) + def _display_available_roles(self, list_json): + """Display all roles we can find with a valid argument specification. + + Output is: fqcn role name, entry point, short description + """ + roles = list(list_json.keys()) + entry_point_names = set() + for role in roles: + for entry_point in list_json[role]['entry_points'].keys(): + entry_point_names.add(entry_point) + + max_role_len = 0 + max_ep_len = 0 + + if roles: + max_role_len = max(len(x) for x in roles) + if entry_point_names: + max_ep_len = max(len(x) for x in entry_point_names) + + linelimit = display.columns - max_role_len - max_ep_len - 5 + text = [] + + for role in sorted(roles): + for entry_point, desc in iteritems(list_json[role]['entry_points']): + if len(desc) > linelimit: + desc = desc[:linelimit] + '...' + text.append("%-*s %-*s %s" % (max_role_len, role, + max_ep_len, entry_point, + desc)) + + # display results + DocCLI.pager("\n".join(text)) + + def _display_role_doc(self, role_json): + roles = list(role_json.keys()) + text = [] + for role in roles: + text += self.get_role_man_text(role, role_json[role]) + + # display results + DocCLI.pager("\n".join(text)) + @staticmethod def _list_keywords(): return from_yaml(pkgutil.get_data('ansible', 'keyword_desc.yml')) @@ -315,11 +566,27 @@ class DocCLI(CLI): super(DocCLI, self).run() + basedir = context.CLIARGS['basedir'] plugin_type = context.CLIARGS['type'] do_json = context.CLIARGS['json_format'] + roles_path = context.CLIARGS['roles_path'] listing = context.CLIARGS['list_files'] or context.CLIARGS['list_dir'] or context.CLIARGS['dump'] docs = {} + if basedir: + AnsibleCollectionConfig.playbook_paths = basedir + + # Add any 'roles' subdir in playbook dir to the roles search path. + # And as a last resort, add the playbook dir itself. Order being: + # - 'roles' subdir of playbook dir + # - DEFAULT_ROLES_PATH + # - playbook dir + # NOTE: This matches logic in RoleDefinition._load_role_path() method. + subdir = os.path.join(basedir, "roles") + if os.path.isdir(subdir): + roles_path = (subdir,) + roles_path + roles_path = roles_path + (basedir,) + if plugin_type not in TARGET_OPTIONS: raise AnsibleOptionsError("Unknown or undocumentable plugin type: %s" % plugin_type) elif plugin_type == 'keyword': @@ -328,6 +595,20 @@ class DocCLI(CLI): docs = DocCLI._list_keywords() else: docs = DocCLI._get_keywords_docs(context.CLIARGS['args']) + elif plugin_type == 'role': + if context.CLIARGS['list_dir']: + # If an argument was given with --list, it is a collection filter + coll_filter = None + if len(context.CLIARGS['args']) == 1: + coll_filter = context.CLIARGS['args'][0] + if not AnsibleCollectionRef.is_valid_collection_name(coll_filter): + raise AnsibleError('Invalid collection name (must be of the form namespace.collection): {0}'.format(coll_filter)) + elif len(context.CLIARGS['args']) > 1: + raise AnsibleOptionsError("Only a single collection filter is supported.") + + docs = self._create_role_list(roles_path, collection_filter=coll_filter) + else: + docs = self._create_role_doc(context.CLIARGS['args'], roles_path, context.CLIARGS['entry_point']) else: loader = getattr(plugin_loader, '%s_loader' % plugin_type) @@ -367,6 +648,11 @@ class DocCLI(CLI): text.append(textret) else: display.warning("No valid documentation was retrieved from '%s'" % plugin) + elif plugin_type == 'role': + if context.CLIARGS['list_dir'] and docs: + self._display_available_roles(docs) + elif docs: + self._display_role_doc(docs) elif docs: text = DocCLI._dump_yaml(docs, '') @@ -713,6 +999,62 @@ class DocCLI(CLI): if not suboptions: text.append('') + def get_role_man_text(self, role, role_json): + '''Generate text for the supplied role suitable for display. + + This is similar to get_man_text(), but roles are different enough that we have + a separate method for formatting their display. + + :param role: The role name. + :param role_json: The JSON for the given role as returned from _create_role_doc(). + + :returns: A array of text suitable for displaying to screen. + ''' + text = [] + opt_indent = " " + pad = display.columns * 0.20 + limit = max(display.columns - int(pad), 70) + + text.append("> %s (%s)\n" % (role.upper(), role_json.get('path'))) + + for entry_point in role_json['entry_points']: + doc = role_json['entry_points'][entry_point] + + if doc.get('short_description'): + text.append("ENTRY POINT: %s - %s\n" % (entry_point, doc.get('short_description'))) + else: + text.append("ENTRY POINT: %s\n" % entry_point) + + if doc.get('description'): + if isinstance(doc['description'], list): + desc = " ".join(doc['description']) + else: + desc = doc['description'] + + text.append("%s\n" % textwrap.fill(DocCLI.tty_ify(desc), + limit, initial_indent=opt_indent, + subsequent_indent=opt_indent)) + if doc.get('options'): + text.append("OPTIONS (= is mandatory):\n") + DocCLI.add_fields(text, doc.pop('options'), limit, opt_indent) + text.append('') + + # generic elements we will handle identically + for k in ('author',): + if k not in doc: + continue + if isinstance(doc[k], string_types): + text.append('%s: %s' % (k.upper(), textwrap.fill(DocCLI.tty_ify(doc[k]), + limit - (len(k) + 2), subsequent_indent=opt_indent))) + elif isinstance(doc[k], (list, tuple)): + text.append('%s: %s' % (k.upper(), ', '.join(doc[k]))) + else: + # use empty indent since this affects the start of the yaml doc, not it's keys + text.append(DocCLI._dump_yaml({k.upper(): doc[k]}, '')) + text.append('') + + return text + @staticmethod def get_man_text(doc, collection_name='', plugin_type=''): # Create a copy so we don't modify the original diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/argument_specs.yml b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/argument_specs.yml new file mode 100644 index 00000000000..5b1b7049d20 --- /dev/null +++ b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/argument_specs.yml @@ -0,0 +1,22 @@ +--- +main: + short_description: testns.testcol.testrole short description for main entry point + description: + - Longer description for testns.testcol.testrole main entry point. + author: Ansible Core (@ansible) + options: + opt1: + description: opt1 description + type: "str" + required: true + +alternate: + short_description: testns.testcol.testrole short description for alternate entry point + description: + - Longer description for testns.testcol.testrole alternate entry point. + author: Ansible Core (@ansible) + options: + altopt1: + description: altopt1 description + type: "int" + required: true diff --git a/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty b/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-doc/fakecollrole.output b/test/integration/targets/ansible-doc/fakecollrole.output new file mode 100644 index 00000000000..fdd6a2dda63 --- /dev/null +++ b/test/integration/targets/ansible-doc/fakecollrole.output @@ -0,0 +1,16 @@ +> TESTNS.TESTCOL.TESTROLE (/ansible/test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol) + +ENTRY POINT: alternate - testns.testcol.testrole short description for alternate entry point + + Longer description for testns.testcol.testrole alternate entry + point. + +OPTIONS (= is mandatory): + += altopt1 + altopt1 description + + type: int + + +AUTHOR: Ansible Core (@ansible) diff --git a/test/integration/targets/ansible-doc/fakerole.output b/test/integration/targets/ansible-doc/fakerole.output new file mode 100644 index 00000000000..81db9a63fe8 --- /dev/null +++ b/test/integration/targets/ansible-doc/fakerole.output @@ -0,0 +1,32 @@ +> TEST_ROLE1 (/ansible/test/integration/targets/ansible-doc/roles/normal_role1) + +ENTRY POINT: main - test_role1 from roles subdir + + In to am attended desirous raptures *declared* diverted + confined at. Collected instantly remaining up certainly to + `necessary' as. Over walk dull into son boy door went new. At + or happiness commanded daughters as. Is `handsome' an declared + at received in extended vicinity subjects. Into miss on he + over been late pain an. Only week bore boy what fat case left + use. Match round scale now style far times. Your me past an + much. + +OPTIONS (= is mandatory): + += myopt1 + First option. + + type: str + +- myopt2 + Second option + [Default: 8000] + type: int + +- myopt3 + Third option. + (Choices: choice1, choice2)[Default: (null)] + type: str + + +AUTHOR: John Doe (@john), Jane Doe (@jane) diff --git a/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml b/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml new file mode 100644 index 00000000000..6fa9c74cbf1 --- /dev/null +++ b/test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml @@ -0,0 +1,33 @@ +--- +main: + short_description: test_role1 from roles subdir + description: + - In to am attended desirous raptures B(declared) diverted confined at. Collected instantly remaining + up certainly to C(necessary) as. Over walk dull into son boy door went new. + - At or happiness commanded daughters as. Is I(handsome) an declared at received in extended vicinity + subjects. Into miss on he over been late pain an. Only week bore boy what fat case left use. Match round + scale now style far times. Your me past an much. + author: + - John Doe (@john) + - Jane Doe (@jane) + + options: + myopt1: + description: + - First option. + type: "str" + required: true + + myopt2: + description: + - Second option + type: "int" + default: 8000 + + myopt3: + description: + - Third option. + type: "str" + choices: + - choice1 + - choice2 diff --git a/test/integration/targets/ansible-doc/roles/test_role2/meta/empty b/test/integration/targets/ansible-doc/roles/test_role2/meta/empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/ansible-doc/runme.sh b/test/integration/targets/ansible-doc/runme.sh index c4bea83d4c3..4d5e915cb63 100755 --- a/test/integration/targets/ansible-doc/runme.sh +++ b/test/integration/targets/ansible-doc/runme.sh @@ -14,8 +14,9 @@ unset ANSIBLE_PLAYBOOK_DIR cd "$(dirname "$0")" # test module docs from collection -current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule)" -expected_out="$(cat fakemodule.output)" +# we use sed to strip the module path from the first line +current_out="$(ansible-doc --playbook-dir ./ testns.testcol.fakemodule | sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/')" +expected_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.FAKEMODULE\).*(.*)$/\1/' fakemodule.output)" test "$current_out" == "$expected_out" # ensure we do work with valid collection name for list @@ -45,4 +46,33 @@ do justcol=$(ansible-doc -l -t ${ptype} --playbook-dir ./ testns|wc -l) test "$justcol" -eq 1 done + +#### test role functionality + +# Test role text output +# we use sed to strip the role path from the first line +current_role_out="$(ansible-doc -t role -r ./roles test_role1 | sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/')" +expected_role_out="$(sed '1 s/\(^> TEST_ROLE1\).*(.*)$/\1/' fakerole.output)" +test "$current_role_out" == "$expected_role_out" + +# Two collection roles are defined, but only 1 has a role arg spec with 2 entry points +output=$(ansible-doc -t role -l --playbook-dir . testns.testcol | wc -l) +test "$output" -eq 2 + +# Include normal roles (no collection filter) +output=$(ansible-doc -t role -l --playbook-dir . | wc -l) +test "$output" -eq 3 + +# Test that a role in the playbook dir with the same name as a role in the +# 'roles' subdir of the playbook dir does not appear (lower precedence). +output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from roles subdir") +test "$output" -eq 1 +output=$(ansible-doc -t role -l --playbook-dir . | grep -c "test_role1 from playbook dir" || true) +test "$output" -eq 0 + +# Test entry point filter +current_role_out="$(ansible-doc -t role --playbook-dir . testns.testcol.testrole -e alternate| sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/')" +expected_role_out="$(sed '1 s/\(^> TESTNS\.TESTCOL\.TESTROLE\).*(.*)$/\1/' fakecollrole.output)" +test "$current_role_out" == "$expected_role_out" + ) diff --git a/test/integration/targets/ansible-doc/test_role1/README.txt b/test/integration/targets/ansible-doc/test_role1/README.txt new file mode 100644 index 00000000000..98983c8cee4 --- /dev/null +++ b/test/integration/targets/ansible-doc/test_role1/README.txt @@ -0,0 +1,3 @@ +Test role that exists in the playbook directory so we can validate +that a role of the same name that exists in the 'roles' subdirectory +will take precedence over this one. diff --git a/test/integration/targets/ansible-doc/test_role1/meta/argument_specs.yml b/test/integration/targets/ansible-doc/test_role1/meta/argument_specs.yml new file mode 100644 index 00000000000..9a357587557 --- /dev/null +++ b/test/integration/targets/ansible-doc/test_role1/meta/argument_specs.yml @@ -0,0 +1,4 @@ +--- +main: + short_description: test_role1 from playbook dir + description: This should not appear in `ansible-doc --list` output. diff --git a/test/integration/targets/ansible/playbookdir_cfg.ini b/test/integration/targets/ansible/playbookdir_cfg.ini index f4bf8af895b..16670c5b839 100644 --- a/test/integration/targets/ansible/playbookdir_cfg.ini +++ b/test/integration/targets/ansible/playbookdir_cfg.ini @@ -1,2 +1,2 @@ [defaults] -playbook_dir = /tmp +playbook_dir = /doesnotexist/tmp diff --git a/test/integration/targets/ansible/runme.sh b/test/integration/targets/ansible/runme.sh index 42130961eb3..fc79e33e73b 100755 --- a/test/integration/targets/ansible/runme.sh +++ b/test/integration/targets/ansible/runme.sh @@ -25,13 +25,13 @@ ansible-config view -c ./no-extension 2> err2.txt || grep -q 'Unsupported config rm -f err*.txt # test setting playbook_dir via envvar -ANSIBLE_PLAYBOOK_DIR=/tmp ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/tmp"' +ANSIBLE_PLAYBOOK_DIR=/doesnotexist/tmp ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"' # test setting playbook_dir via cmdline -ansible localhost -m debug -a var=playbook_dir --playbook-dir=/tmp | grep '"playbook_dir": "/tmp"' +ansible localhost -m debug -a var=playbook_dir --playbook-dir=/doesnotexist/tmp | grep '"playbook_dir": "/doesnotexist/tmp"' # test setting playbook dir via ansible.cfg -env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/tmp"' +env -u ANSIBLE_PLAYBOOK_DIR ANSIBLE_CONFIG=./playbookdir_cfg.ini ansible localhost -m debug -a var=playbook_dir | grep '"playbook_dir": "/doesnotexist/tmp"' # test adhoc callback triggers ANSIBLE_STDOUT_CALLBACK=callback_debug ANSIBLE_LOAD_CALLBACK_PLUGINS=1 ansible --playbook-dir . testhost -i ../../inventory -m ping | grep -E '^v2_' | diff -u adhoc-callback.stdout -