summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatt Davis <nitzmahone@users.noreply.github.com>2019-08-29 01:31:40 +0200
committerGitHub <noreply@github.com>2019-08-29 01:31:40 +0200
commit7d1a981b61eb996c64c406a92f73022d4d3a041b (patch)
tree1f41c44bc833f8ce9e45127a1a1aaac6f70abd84
parentFix TypeError in ec2_group.py for Python3 when sorting dictionary list (#59844) (diff)
downloadansible-7d1a981b61eb996c64c406a92f73022d4d3a041b.tar.xz
ansible-7d1a981b61eb996c64c406a92f73022d4d3a041b.zip
default collection support (#61415)
* default collection support * playbooks run from inside a registered collection will set that collection as the first item in the search order (as will all non-collection roles) * this allows easy migration of runme.sh style playbook/role integration tests to collections without the playbooks/roles needing to know the name of their enclosing collection * ignore bogus sanity error * filed #61460 * fixed task unit test failure * don't append an empty collections list to the ds * ignore leftover local_action in mod_args ds action parsing * fix async_extra_data test to not require ssh and bogus locale * disable default collection test under Windows * ensure collection location FS code is always bytes * add changelog
-rw-r--r--changelogs/fragments/default_collection.yml3
-rw-r--r--lib/ansible/cli/playbook.py8
-rw-r--r--lib/ansible/parsing/mod_args.py8
-rw-r--r--lib/ansible/playbook/collectionsearch.py36
-rw-r--r--lib/ansible/playbook/helpers.py8
-rw-r--r--lib/ansible/playbook/role/__init__.py12
-rw-r--r--lib/ansible/playbook/task.py31
-rw-r--r--lib/ansible/playbook/task_include.py2
-rw-r--r--lib/ansible/plugins/action/__init__.py15
-rw-r--r--lib/ansible/utils/collection_loader.py72
-rw-r--r--test/integration/targets/async_extra_data/aliases3
-rw-r--r--test/integration/targets/async_extra_data/inventory1
-rw-r--r--test/integration/targets/async_extra_data/library/junkping.py15
-rwxr-xr-xtest/integration/targets/async_extra_data/runme.sh8
-rw-r--r--test/integration/targets/async_extra_data/test_async.yml6
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml49
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py13
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml29
-rw-r--r--test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml7
-rw-r--r--test/integration/targets/collections/posix.yml9
-rwxr-xr-xtest/integration/targets/collections/runme.sh6
-rw-r--r--test/sanity/ignore.txt1
22 files changed, 278 insertions, 64 deletions
diff --git a/changelogs/fragments/default_collection.yml b/changelogs/fragments/default_collection.yml
new file mode 100644
index 0000000000..601bd887b7
--- /dev/null
+++ b/changelogs/fragments/default_collection.yml
@@ -0,0 +1,3 @@
+minor_changes:
+ - default collection - a playbook run inside a collection (eg, as part of a runme.sh integration test) will
+ first search the containing collection for unqualified module/action references (https://github.com/ansible/ansible/pull/61415)
diff --git a/lib/ansible/cli/playbook.py b/lib/ansible/cli/playbook.py
index 887c010f16..300d913349 100644
--- a/lib/ansible/cli/playbook.py
+++ b/lib/ansible/cli/playbook.py
@@ -16,7 +16,7 @@ from ansible.executor.playbook_executor import PlaybookExecutor
from ansible.module_utils._text import to_bytes
from ansible.playbook.block import Block
from ansible.utils.display import Display
-from ansible.utils.collection_loader import set_collection_playbook_paths
+from ansible.utils.collection_loader import AnsibleCollectionLoader, get_collection_name_from_path, set_collection_playbook_paths
from ansible.plugins.loader import add_all_plugin_dirs
@@ -92,6 +92,12 @@ class PlaybookCLI(CLI):
set_collection_playbook_paths(b_playbook_dirs)
+ playbook_collection = get_collection_name_from_path(b_playbook_dirs[0])
+
+ if playbook_collection:
+ display.warning("running playbook inside collection {0}".format(playbook_collection))
+ AnsibleCollectionLoader().set_default_collection(playbook_collection)
+
# don't deal with privilege escalation or passwords when we don't need to
if not (context.CLIARGS['listhosts'] or context.CLIARGS['listtasks'] or
context.CLIARGS['listtags'] or context.CLIARGS['syntax']):
diff --git a/lib/ansible/parsing/mod_args.py b/lib/ansible/parsing/mod_args.py
index da1ab26db0..e01eb7b8c5 100644
--- a/lib/ansible/parsing/mod_args.py
+++ b/lib/ansible/parsing/mod_args.py
@@ -121,8 +121,8 @@ class ModuleArgsParser:
# store the valid Task/Handler attrs for quick access
self._task_attrs = set(Task._valid_attrs.keys())
self._task_attrs.update(set(Handler._valid_attrs.keys()))
- # HACK: why is static not a FieldAttribute on task with a post-validate to bomb if not include/import?
- self._task_attrs.add('static')
+ # HACK: why are these not FieldAttributes on task with a post-validate to check usage?
+ self._task_attrs.update(['local_action', 'static'])
self._task_attrs = frozenset(self._task_attrs)
def _split_module_string(self, module_string):
@@ -259,7 +259,7 @@ class ModuleArgsParser:
return (action, args)
- def parse(self):
+ def parse(self, skip_action_validation=False):
'''
Given a task in one of the supported forms, parses and returns
returns the action, arguments, and delegate_to values for the
@@ -300,7 +300,7 @@ class ModuleArgsParser:
# walk the filtered input dictionary to see if we recognize a module name
for item, value in iteritems(non_task_ds):
- if item in BUILTIN_TASKS or action_loader.has_plugin(item, collection_list=self._collection_list) or \
+ if item in BUILTIN_TASKS or skip_action_validation or action_loader.has_plugin(item, collection_list=self._collection_list) or \
module_loader.has_plugin(item, collection_list=self._collection_list):
# finding more than one module name is a problem
if action is not None:
diff --git a/lib/ansible/playbook/collectionsearch.py b/lib/ansible/playbook/collectionsearch.py
index 245d98117b..93b80a8665 100644
--- a/lib/ansible/playbook/collectionsearch.py
+++ b/lib/ansible/playbook/collectionsearch.py
@@ -6,21 +6,39 @@ __metaclass__ = type
from ansible.module_utils.six import string_types
from ansible.playbook.attribute import FieldAttribute
+from ansible.utils.collection_loader import AnsibleCollectionLoader
+
+
+def _ensure_default_collection(collection_list=None):
+ default_collection = AnsibleCollectionLoader().default_collection
+
+ if collection_list is None:
+ collection_list = []
+
+ if default_collection: # FIXME: exclude role tasks?
+ if isinstance(collection_list, string_types):
+ collection_list = [collection_list]
+
+ if default_collection not in collection_list:
+ collection_list.insert(0, default_collection)
+
+ # if there's something in the list, ensure that builtin or legacy is always there too
+ if collection_list and 'ansible.builtin' not in collection_list and 'ansible.legacy' not in collection_list:
+ collection_list.append('ansible.legacy')
+
+ return collection_list
class CollectionSearch:
+
# this needs to be populated before we can resolve tasks/roles/etc
- _collections = FieldAttribute(isa='list', listof=string_types, priority=100)
+ _collections = FieldAttribute(isa='list', listof=string_types, priority=100, default=_ensure_default_collection)
def _load_collections(self, attr, ds):
- if not ds:
- # if empty/None, just return whatever was there; legacy behavior will do the right thing
- return ds
-
- if not isinstance(ds, list):
- ds = [ds]
+ # this will only be called if someone specified a value; call the shared value
+ _ensure_default_collection(collection_list=ds)
- if 'ansible.builtin' not in ds and 'ansible.legacy' not in ds:
- ds.append('ansible.legacy')
+ if not ds: # don't return an empty collection list, just return None
+ return None
return ds
diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py
index 85e17ba3e2..197329bc74 100644
--- a/lib/ansible/playbook/helpers.py
+++ b/lib/ansible/playbook/helpers.py
@@ -25,6 +25,7 @@ from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, Ansible
from ansible.module_utils._text import to_native
from ansible.module_utils.six import string_types
from ansible.parsing.mod_args import ModuleArgsParser
+from ansible.utils.collection_loader import AnsibleCollectionLoader
from ansible.utils.display import Display
display = Display()
@@ -117,12 +118,9 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
)
task_list.append(t)
else:
- collection_list = task_ds.get('collections')
- if collection_list is None and block is not None and block.collections:
- collection_list = block.collections
- args_parser = ModuleArgsParser(task_ds, collection_list=collection_list)
+ args_parser = ModuleArgsParser(task_ds)
try:
- (action, args, delegate_to) = args_parser.parse()
+ (action, args, delegate_to) = args_parser.parse(skip_action_validation=True)
except AnsibleParserError as e:
# if the raises exception was created with obj=ds args, then it includes the detail
# so we dont need to add it so we can just re raise.
diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py
index 9230f48a30..d335d27034 100644
--- a/lib/ansible/playbook/role/__init__.py
+++ b/lib/ansible/playbook/role/__init__.py
@@ -32,6 +32,7 @@ from ansible.playbook.helpers import load_list_of_blocks
from ansible.playbook.role.metadata import RoleMetadata
from ansible.playbook.taggable import Taggable
from ansible.plugins.loader import add_all_plugin_dirs
+from ansible.utils.collection_loader import AnsibleCollectionLoader
from ansible.utils.vars import combine_vars
@@ -224,20 +225,23 @@ class Role(Base, Conditional, Taggable, CollectionSearch):
# configure plugin/collection loading; either prepend the current role's collection or configure legacy plugin loading
# FIXME: need exception for explicit ansible.legacy?
- if self._role_collection:
+ if self._role_collection: # this is a collection-hosted role
self.collections.insert(0, self._role_collection)
- else:
+ else: # this is a legacy role, but set the default collection if there is one
+ default_collection = AnsibleCollectionLoader().default_collection
+ if default_collection:
+ self.collections.insert(0, default_collection)
# legacy role, ensure all plugin dirs under the role are added to plugin search path
add_all_plugin_dirs(self._role_path)
# collections can be specified in metadata for legacy or collection-hosted roles
if self._metadata.collections:
- self.collections.extend(self._metadata.collections)
+ self.collections.extend((c for c in self._metadata.collections if c not in self.collections))
# if any collections were specified, ensure that core or legacy synthetic collections are always included
if self.collections:
# default append collection is core for collection-hosted roles, legacy for others
- default_append_collection = 'ansible.builtin' if self.collections else 'ansible.legacy'
+ default_append_collection = 'ansible.builtin' if self._role_collection else 'ansible.legacy'
if 'ansible.builtin' not in self.collections and 'ansible.legacy' not in self.collections:
self.collections.append(default_append_collection)
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
index 6380b300c7..e9e31a9a92 100644
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -36,6 +36,7 @@ from ansible.playbook.conditional import Conditional
from ansible.playbook.loop_control import LoopControl
from ansible.playbook.role import Role
from ansible.playbook.taggable import Taggable
+from ansible.utils.collection_loader import AnsibleCollectionLoader
from ansible.utils.display import Display
from ansible.utils.sentinel import Sentinel
@@ -177,10 +178,35 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if isinstance(ds, AnsibleBaseYAMLObject):
new_ds.ansible_pos = ds.ansible_pos
+ # since this affects the task action parsing, we have to resolve in preprocess instead of in typical validator
+ default_collection = AnsibleCollectionLoader().default_collection
+
+ # use the parent value if our ds doesn't define it
+ collections_list = ds.get('collections', self.collections)
+
+ if collections_list is None:
+ collections_list = []
+
+ if isinstance(collections_list, string_types):
+ collections_list = [collections_list]
+
+ if default_collection and not self._role: # FIXME: and not a collections role
+ if collections_list:
+ if default_collection not in collections_list:
+ collections_list.insert(0, default_collection)
+ else:
+ collections_list = [default_collection]
+
+ if collections_list and 'ansible.builtin' not in collections_list and 'ansible.legacy' not in collections_list:
+ collections_list.append('ansible.legacy')
+
+ if collections_list:
+ ds['collections'] = collections_list
+
# use the args parsing class to determine the action, args,
# and the delegate_to value from the various possible forms
# supported as legacy
- args_parser = ModuleArgsParser(task_ds=ds, collection_list=self.collections)
+ args_parser = ModuleArgsParser(task_ds=ds, collection_list=collections_list)
try:
(action, args, delegate_to) = args_parser.parse()
except AnsibleParserError as e:
@@ -264,6 +290,9 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
if self._parent:
self._parent.post_validate(templar)
+ if AnsibleCollectionLoader().default_collection:
+ pass
+
super(Task, self).post_validate(templar)
def _post_validate_loop(self, attr, value, templar):
diff --git a/lib/ansible/playbook/task_include.py b/lib/ansible/playbook/task_include.py
index 365ce30bb6..59c77a8ef1 100644
--- a/lib/ansible/playbook/task_include.py
+++ b/lib/ansible/playbook/task_include.py
@@ -42,7 +42,7 @@ class TaskInclude(Task):
BASE = frozenset(('file', '_raw_params')) # directly assigned
OTHER_ARGS = frozenset(('apply',)) # assigned to matching property
VALID_ARGS = BASE.union(OTHER_ARGS) # all valid args
- VALID_INCLUDE_KEYWORDS = frozenset(('action', 'args', 'debugger', 'ignore_errors', 'loop', 'loop_control',
+ VALID_INCLUDE_KEYWORDS = frozenset(('action', 'args', 'collections', 'debugger', 'ignore_errors', 'loop', 'loop_control',
'loop_with', 'name', 'no_log', 'register', 'run_once', 'tags', 'vars',
'when'))
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py
index 0358df94d7..cc1136e8f7 100644
--- a/lib/ansible/plugins/action/__init__.py
+++ b/lib/ansible/plugins/action/__init__.py
@@ -169,20 +169,7 @@ class ActionBase(with_metaclass(ABCMeta, object)):
if module_path:
break
else: # This is a for-else: http://bit.ly/1ElPkyg
- # Use Windows version of ping module to check module paths when
- # using a connection that supports .ps1 suffixes. We check specifically
- # for win_ping here, otherwise the code would look for ping.ps1
- if '.ps1' in self._connection.module_implementation_preferences:
- ping_module = 'win_ping'
- else:
- ping_module = 'ping'
- module_path2 = self._shared_loader_obj.module_loader.find_plugin(ping_module, self._connection.module_implementation_preferences)
- if module_path2 is not None:
- raise AnsibleError("The module %s was not found in configured module paths" % (module_name))
- else:
- raise AnsibleError("The module %s was not found in configured module paths. "
- "Additionally, core modules are missing. If this is a checkout, "
- "run 'git pull --rebase' to correct this problem." % (module_name))
+ raise AnsibleError("The module %s was not found in configured module paths" % (module_name))
# insert shared code and arguments into the module
final_environment = dict()
diff --git a/lib/ansible/utils/collection_loader.py b/lib/ansible/utils/collection_loader.py
index 0eae71960d..daa64b60e8 100644
--- a/lib/ansible/utils/collection_loader.py
+++ b/lib/ansible/utils/collection_loader.py
@@ -48,6 +48,7 @@ class AnsibleCollectionLoader(with_metaclass(Singleton, object)):
self._n_configured_paths = [to_native(os.path.expanduser(p), errors='surrogate_or_strict') for p in self._n_configured_paths]
self._n_playbook_paths = []
+ self._default_collection = None
# pre-inject grafted package maps so we can force them to use the right loader instead of potentially delegating to a "normal" loader
for syn_pkg_def in (p for p in iteritems(_SYNTHETIC_PACKAGES) if p[1].get('graft')):
pkg_name = syn_pkg_def[0]
@@ -70,6 +71,14 @@ class AnsibleCollectionLoader(with_metaclass(Singleton, object)):
def n_collection_paths(self):
return self._n_playbook_paths + self._n_configured_paths
+ def get_collection_path(self, collection_name):
+ if not AnsibleCollectionRef.is_valid_collection_name(collection_name):
+ raise ValueError('{0} is not a valid collection name'.format(to_native(collection_name)))
+
+ m = import_module('ansible_collections.{0}'.format(collection_name))
+
+ return m.__file__
+
def set_playbook_paths(self, b_playbook_paths):
if isinstance(b_playbook_paths, string_types):
b_playbook_paths = [b_playbook_paths]
@@ -81,6 +90,15 @@ class AnsibleCollectionLoader(with_metaclass(Singleton, object)):
self._n_playbook_paths = [os.path.join(to_native(p), 'collections') for p in b_playbook_paths if not (p in added_paths or added_paths.add(p))]
# FIXME: only allow setting this once, or handle any necessary cache/package path invalidations internally?
+ # FIXME: is there a better place to store this?
+ # FIXME: only allow setting this once
+ def set_default_collection(self, collection_name):
+ self._default_collection = collection_name
+
+ @property
+ def default_collection(self):
+ return self._default_collection
+
def find_module(self, fullname, path=None):
# this loader is only concerned with items under the Ansible Collections namespace hierarchy, ignore others
if fullname.startswith('ansible_collections.') or fullname == 'ansible_collections':
@@ -432,14 +450,17 @@ def get_collection_role_path(role_name, collection_list=None):
# looks like a valid qualified collection ref; skip the collection_list
role = acr.resource
collection_list = [acr.collection]
+ subdirs = acr.subdirs
+ resource = acr.resource
elif not collection_list:
return None # not a FQ role and no collection search list spec'd, nothing to do
else:
- role = role_name # treat as unqualified, loop through the collection search list to try and resolve
+ resource = role_name # treat as unqualified, loop through the collection search list to try and resolve
+ subdirs = ''
for collection_name in collection_list:
try:
- acr = AnsibleCollectionRef(collection_name=collection_name, subdirs=acr.subdirs, resource=acr.resource, ref_type=acr.ref_type)
+ acr = AnsibleCollectionRef(collection_name=collection_name, subdirs=subdirs, resource=resource, ref_type='role')
# FIXME: error handling/logging; need to catch any import failures and move along
# FIXME: this line shouldn't be necessary, but py2 pkgutil.get_data is delegating back to built-in loader when it shouldn't
@@ -448,7 +469,7 @@ def get_collection_role_path(role_name, collection_list=None):
if pkg is not None:
# the package is now loaded, get the collection's package and ask where it lives
path = os.path.dirname(to_bytes(sys.modules[acr.n_python_package_name].__file__, errors='surrogate_or_strict'))
- return role, to_text(path, errors='surrogate_or_strict'), collection_name
+ return resource, to_text(path, errors='surrogate_or_strict'), collection_name
except IOError:
continue
@@ -459,5 +480,50 @@ def get_collection_role_path(role_name, collection_list=None):
return None
+_N_COLLECTION_PATH_RE = re.compile(r'/ansible_collections/([^/]+)/([^/]+)')
+
+
+def get_collection_name_from_path(path):
+ """
+ Return the containing collection name for a given path, or None if the path is not below a configured collection, or
+ the collection cannot be loaded (eg, the collection is masked by another of the same name higher in the configured
+ collection roots).
+ :param n_path: native-string path to evaluate for collection containment
+ :return: collection name or None
+ """
+ n_collection_paths = [to_native(os.path.realpath(to_bytes(p))) for p in AnsibleCollectionLoader().n_collection_paths]
+
+ b_path = os.path.realpath(to_bytes(path))
+ n_path = to_native(b_path)
+
+ for coll_path in n_collection_paths:
+ common_prefix = to_native(os.path.commonprefix([b_path, to_bytes(coll_path)]))
+ if common_prefix == coll_path:
+ # strip off the common prefix (handle weird testing cases of nested collection roots, eg)
+ collection_remnant = n_path[len(coll_path):]
+ # commonprefix may include the trailing /, prepend to the remnant if necessary (eg trailing / on root)
+ if collection_remnant[0] != '/':
+ collection_remnant = '/' + collection_remnant
+ # the path lives under this collection root, see if it maps to a collection
+ found_collection = _N_COLLECTION_PATH_RE.search(collection_remnant)
+ if not found_collection:
+ continue
+ n_collection_name = '{0}.{1}'.format(*found_collection.groups())
+
+ loaded_collection_path = AnsibleCollectionLoader().get_collection_path(n_collection_name)
+
+ if not loaded_collection_path:
+ return None
+
+ # ensure we're using the canonical real path, with the bogus __synthetic__ stripped off
+ b_loaded_collection_path = os.path.dirname(os.path.realpath(to_bytes(loaded_collection_path)))
+
+ # if the collection path prefix matches the path prefix we were passed, it's the same collection that's loaded
+ if os.path.commonprefix([b_path, b_loaded_collection_path]) == b_loaded_collection_path:
+ return n_collection_name
+
+ return None # if not, it's a collection, but not the same collection the loader sees, so ignore it
+
+
def set_collection_playbook_paths(b_playbook_paths):
AnsibleCollectionLoader().set_playbook_paths(b_playbook_paths)
diff --git a/test/integration/targets/async_extra_data/aliases b/test/integration/targets/async_extra_data/aliases
index a5c224e448..b59832142f 100644
--- a/test/integration/targets/async_extra_data/aliases
+++ b/test/integration/targets/async_extra_data/aliases
@@ -1,4 +1 @@
-needs/ssh
shippable/posix/group3
-skip/freebsd
-skip/osx
diff --git a/test/integration/targets/async_extra_data/inventory b/test/integration/targets/async_extra_data/inventory
deleted file mode 100644
index 880bcda951..0000000000
--- a/test/integration/targets/async_extra_data/inventory
+++ /dev/null
@@ -1 +0,0 @@
-localhost ansible_connection=ssh ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/test/integration/targets/async_extra_data/library/junkping.py b/test/integration/targets/async_extra_data/library/junkping.py
new file mode 100644
index 0000000000..b61d965da9
--- /dev/null
+++ b/test/integration/targets/async_extra_data/library/junkping.py
@@ -0,0 +1,15 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print("junk_before_module_output")
+ print(json.dumps(dict(changed=False, source='user')))
+ print("junk_after_module_output")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/async_extra_data/runme.sh b/test/integration/targets/async_extra_data/runme.sh
index 01bde32d93..46132731fe 100755
--- a/test/integration/targets/async_extra_data/runme.sh
+++ b/test/integration/targets/async_extra_data/runme.sh
@@ -2,8 +2,6 @@
set -eux
-# Verify that extra data before module JSON output during async call is ignored.
-ANSIBLE_DEBUG=0 LC_ALL=bogus ansible-playbook test_async.yml -i inventory -v "$@"
-# Verify that the warning exists by examining debug output.
-ANSIBLE_DEBUG=1 LC_ALL=bogus ansible-playbook test_async.yml -i inventory -v "$@" \
- | grep 'bash: warning: setlocale: LC_ALL: cannot change locale (bogus)' > /dev/null
+# Verify that extra data before module JSON output during async call is ignored, and that the warning exists.
+ANSIBLE_DEBUG=0 ansible-playbook -i ../../inventory test_async.yml -v "$@" \
+ | grep 'junk after the JSON data: junk_after_module_output'
diff --git a/test/integration/targets/async_extra_data/test_async.yml b/test/integration/targets/async_extra_data/test_async.yml
index e54ffd3d47..480a2a651c 100644
--- a/test/integration/targets/async_extra_data/test_async.yml
+++ b/test/integration/targets/async_extra_data/test_async.yml
@@ -1,9 +1,9 @@
-- hosts: localhost
+- hosts: testhost
gather_facts: false
tasks:
# make sure non-JSON data before module output is ignored
- - name: async ping with invalid locale via ssh
- ping:
+ - name: async ping wrapped in extra junk
+ junkping:
async: 10
poll: 1
register: result
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml
new file mode 100644
index 0000000000..1d1aee7dc1
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml
@@ -0,0 +1,49 @@
+# verify default collection action/module lookup works
+# since we're running this playbook inside a collection, it will set that collection as the default search for all playboooks
+# and non-collection roles to allow for easy migration of old integration tests to collections
+- hosts: testhost
+ tasks:
+ - testmodule:
+
+- hosts: testhost
+ vars:
+ test_role_input: task static default collection
+ tasks:
+ - import_role:
+ name: testrole # unqualified role lookup should work; inheriting from the containing collection
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+ - vars:
+ test_role_input: task static legacy embedded default collection
+ block:
+ - import_role:
+ name: non_coll_role
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+
+- hosts: testhost
+ vars:
+ test_role_input: keyword static default collection
+ roles:
+ - testrole
+ tasks:
+ - debug: var=test_role_input
+ - debug: var=test_role_output
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
+
+- hosts: testhost
+ vars:
+ test_role_input: task dynamic default collection
+ tasks:
+ - include_role:
+ name: testrole # unqualified role lookup should work; inheriting from the containing collection
+ - include_role:
+ name: non_coll_role
+ - assert:
+ that:
+ - testmodule_out_from_non_coll_role is success
+ - embedded_module_out_from_non_coll_role is success
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py
new file mode 100644
index 0000000000..54402d1216
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+
+def main():
+ print(json.dumps(dict(changed=False, source='collection_embedded_non_collection_role')))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml
new file mode 100644
index 0000000000..d41ae90e95
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml
@@ -0,0 +1,29 @@
+- testmodule:
+ register: testmodule_out_from_non_coll_role
+
+- embedded_module:
+ register: embedded_module_out_from_non_coll_role
+
+- name: check collections list from role meta
+ plugin_lookup:
+ register: pluginlookup_out
+
+- debug: var=pluginlookup_out
+
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- assert:
+ that:
+ - test_role_input is not defined or test_role_input == test_role_output.msg
+
+- vars:
+ test_role_input: include another non-coll role
+ block:
+ - include_role:
+ name: non_coll_role_to_call
+
+ - assert:
+ that:
+ - test_role_output.msg == test_role_input
diff --git a/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml
new file mode 100644
index 0000000000..98445ce3aa
--- /dev/null
+++ b/test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml
@@ -0,0 +1,7 @@
+- debug:
+ msg: '{{ test_role_input | default("(undefined)") }}'
+ register: test_role_output
+
+- assert:
+ that:
+ - test_role_input is not defined or test_role_input == test_role_output.msg
diff --git a/test/integration/targets/collections/posix.yml b/test/integration/targets/collections/posix.yml
index 8e9e263059..eac73e2d99 100644
--- a/test/integration/targets/collections/posix.yml
+++ b/test/integration/targets/collections/posix.yml
@@ -193,15 +193,6 @@
- testmodule_out.source == 'user'
- systestmodule_out.source == 'sys'
-# FIXME: this won't work until collections list gets passed through task templar
-# - name: exercise unqualified filters/tests/lookups
-# assert:
-# that:
-# - "'data' | testfilter == 'data_from_userdir'"
-# - "'from_user' is testtest"
-# - lookup('mylookup') == 'lookup_from_user_dir'
-
-
# test keyword-static execution of a FQ collection-backed role with "tasks/main.yaml"
- name: verify collection-backed role execution (keyword static)
hosts: testhost
diff --git a/test/integration/targets/collections/runme.sh b/test/integration/targets/collections/runme.sh
index 7a30b542a0..c5faf154d3 100755
--- a/test/integration/targets/collections/runme.sh
+++ b/test/integration/targets/collections/runme.sh
@@ -7,7 +7,8 @@ export ANSIBLE_GATHERING=explicit
export ANSIBLE_GATHER_SUBSET=minimal
export ANSIBLE_HOST_PATTERN_MISMATCH=error
-# FIXME: just use INVENTORY_PATH as-is once ansible-test sets the right dir
+
+# FUTURE: just use INVENTORY_PATH as-is once ansible-test sets the right dir
ipath=../../$(basename "${INVENTORY_PATH}")
export INVENTORY_PATH="$ipath"
@@ -26,6 +27,9 @@ if [[ ${INVENTORY_PATH} == *.winrm ]]; then
export TEST_PLAYBOOK=windows.yml
else
export TEST_PLAYBOOK=posix.yml
+
+ echo "testing default collection support"
+ ansible-playbook -i "${INVENTORY_PATH}" collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml
fi
# run test playbook
diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt
index 96ab7e472f..e0ee5be2ba 100644
--- a/test/sanity/ignore.txt
+++ b/test/sanity/ignore.txt
@@ -5714,6 +5714,7 @@ lib/ansible/modules/windows/win_webpicmd.ps1 pslint:PSAvoidUsingInvokeExpression
lib/ansible/modules/windows/win_xml.ps1 pslint:PSCustomUseLiteralPath
lib/ansible/parsing/vault/__init__.py pylint:blacklisted-name
lib/ansible/playbook/base.py pylint:blacklisted-name
+lib/ansible/playbook/collectionsearch.py required-and-default-attributes # https://github.com/ansible/ansible/issues/61460
lib/ansible/playbook/helpers.py pylint:blacklisted-name
lib/ansible/playbook/role/__init__.py pylint:blacklisted-name
lib/ansible/plugins/action/aireos.py action-plugin-docs # base class for deprecated network platform modules using `connection: local`