summaryrefslogtreecommitdiff
path: root/lib/ansible/playbook
diff options
context:
space:
mode:
authorMatt Davis <nitzmahone@users.noreply.github.com>2019-03-28 10:41:39 -0700
committerGitHub <noreply@github.com>2019-03-28 10:41:39 -0700
commitf86345f777b8c592c83dd6ef5928ee0ca092a063 (patch)
tree18cd9df7db60d2138edb29c5d0bcb47a3647efa9 /lib/ansible/playbook
parent5173548a9fb53843ed99a283c8c8b21ea9710418 (diff)
downloadansible-f86345f777b8c592c83dd6ef5928ee0ca092a063.tar.gz
Collection content loading (#52194)
* basic plugin loading working (with many hacks) * task collections working * play/block-level collection module/action working * implement PEP302 loader * implicit package support (no need for __init.py__ in collections) * provides future options for secure loading of content that shouldn't execute inside controller (eg, actively ignore __init__.py on content/module paths) * provide hook for synthetic collection setup (eg ansible.core pseudo-collection for specifying built-in plugins without legacy path, etc) * synthetic package support * ansible.core.plugins mapping works, others don't * synthetic collections working for modules/actions * fix direct-load legacy * change base package name to ansible_collections * note * collection role loading * expand paths from installed content root vars * feature complete? * rename ansible.core to ansible.builtin * and various sanity fixes * sanity tweaks * unittest fixes * less grabby error handler on has_plugin * probably need to replace with a or harden callers * fix win_ping test * disable module test with explicit file extension; might be able to support in some scenarios, but can't see any other tests that verify that behavior... * fix unicode conversion issues on py2 * attempt to keep things working-ish on py2.6 * python2.6 test fun round 2 * rename dirs/configs to "collections" * add wrapper dir for content-adjacent * fix pythoncheck to use localhost * unicode tweaks, native/bytes string prefixing * rename COLLECTION_PATHS to COLLECTIONS_PATHS * switch to pathspec * path handling cleanup * change expensive `all` back to or chain * unused import cleanup * quotes tweak * use wrapped iter/len in Jinja proxy * var name expansion * comment seemingly overcomplicated playbook_paths resolution * drop unnecessary conditional nesting * eliminate extraneous local * zap superfluous validation function * use slice for rolespec NS assembly * misc naming/unicode fixes * collection callback loader asks if valid FQ name instead of just '.' * switch collection role resolution behavior to be internally `text` as much as possible * misc fixmes * to_native in exception constructor * (slightly) detangle tuple accumulation mess in module_utils __init__ walker * more misc fixmes * tighten up action dispatch, add unqualified action test * rename Collection mixin to CollectionSearch * (attempt to) avoid potential confusion/conflict with builtin collections, etc * stale fixmes * tighten up pluginloader collections determination * sanity test fixes * ditch regex escape * clarify comment * update default collections paths config entry * use PATH format instead of list * skip integration tests on Python 2.6 ci_complete
Diffstat (limited to 'lib/ansible/playbook')
-rw-r--r--lib/ansible/playbook/__init__.py2
-rw-r--r--lib/ansible/playbook/block.py3
-rw-r--r--lib/ansible/playbook/collectionsearch.py26
-rw-r--r--lib/ansible/playbook/helpers.py8
-rw-r--r--lib/ansible/playbook/play.py3
-rw-r--r--lib/ansible/playbook/role/__init__.py32
-rw-r--r--lib/ansible/playbook/role/definition.py43
-rw-r--r--lib/ansible/playbook/role/include.py9
-rw-r--r--lib/ansible/playbook/role/metadata.py10
-rw-r--r--lib/ansible/playbook/role_include.py7
-rw-r--r--lib/ansible/playbook/task.py5
11 files changed, 115 insertions, 33 deletions
diff --git a/lib/ansible/playbook/__init__.py b/lib/ansible/playbook/__init__.py
index 5c912f6d2a..0321615505 100644
--- a/lib/ansible/playbook/__init__.py
+++ b/lib/ansible/playbook/__init__.py
@@ -23,7 +23,7 @@ import os
from ansible import constants as C
from ansible.errors import AnsibleParserError
-from ansible.module_utils._text import to_bytes, to_text, to_native
+from ansible.module_utils._text import to_text, to_native
from ansible.playbook.play import Play
from ansible.playbook.playbook_include import PlaybookInclude
from ansible.utils.display import Display
diff --git a/lib/ansible/playbook/block.py b/lib/ansible/playbook/block.py
index 418af42ddf..103b7e4b1f 100644
--- a/lib/ansible/playbook/block.py
+++ b/lib/ansible/playbook/block.py
@@ -24,13 +24,14 @@ from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
from ansible.playbook.become import Become
from ansible.playbook.conditional import Conditional
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.helpers import load_list_of_tasks
from ansible.playbook.role import Role
from ansible.playbook.taggable import Taggable
from ansible.utils.sentinel import Sentinel
-class Block(Base, Become, Conditional, Taggable):
+class Block(Base, Become, Conditional, CollectionSearch, Taggable):
# main block fields containing the task lists
_block = FieldAttribute(isa='list', default=list, inherit=False)
diff --git a/lib/ansible/playbook/collectionsearch.py b/lib/ansible/playbook/collectionsearch.py
new file mode 100644
index 0000000000..245d98117b
--- /dev/null
+++ b/lib/ansible/playbook/collectionsearch.py
@@ -0,0 +1,26 @@
+# Copyright: (c) 2019, Ansible Project
+# 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
+
+from ansible.module_utils.six import string_types
+from ansible.playbook.attribute import FieldAttribute
+
+
+class CollectionSearch:
+ # this needs to be populated before we can resolve tasks/roles/etc
+ _collections = FieldAttribute(isa='list', listof=string_types, priority=100)
+
+ 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]
+
+ if 'ansible.builtin' not in ds and 'ansible.legacy' not in ds:
+ ds.append('ansible.legacy')
+
+ return ds
diff --git a/lib/ansible/playbook/helpers.py b/lib/ansible/playbook/helpers.py
index 12b18ae62b..f9b15dc4bb 100644
--- a/lib/ansible/playbook/helpers.py
+++ b/lib/ansible/playbook/helpers.py
@@ -117,7 +117,10 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
)
task_list.append(t)
else:
- args_parser = ModuleArgsParser(task_ds)
+ 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)
try:
(action, args, delegate_to) = args_parser.parse()
except AnsibleParserError as e:
@@ -382,7 +385,8 @@ def load_list_of_roles(ds, play, current_role_path=None, variable_manager=None,
roles = []
for role_def in ds:
- i = RoleInclude.load(role_def, play=play, current_role_path=current_role_path, variable_manager=variable_manager, loader=loader)
+ i = RoleInclude.load(role_def, play=play, current_role_path=current_role_path, variable_manager=variable_manager,
+ loader=loader, collection_list=play.collections)
roles.append(i)
return roles
diff --git a/lib/ansible/playbook/play.py b/lib/ansible/playbook/play.py
index 05c6aea2cb..386f5871c0 100644
--- a/lib/ansible/playbook/play.py
+++ b/lib/ansible/playbook/play.py
@@ -27,6 +27,7 @@ from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
from ansible.playbook.become import Become
from ansible.playbook.block import Block
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.helpers import load_list_of_blocks, load_list_of_roles
from ansible.playbook.role import Role
from ansible.playbook.taggable import Taggable
@@ -39,7 +40,7 @@ display = Display()
__all__ = ['Play']
-class Play(Base, Taggable, Become):
+class Play(Base, Taggable, Become, CollectionSearch):
"""
A play is a language feature that represents a list of roles and/or
diff --git a/lib/ansible/playbook/role/__init__.py b/lib/ansible/playbook/role/__init__.py
index 5a01b453ed..a00db31b59 100644
--- a/lib/ansible/playbook/role/__init__.py
+++ b/lib/ansible/playbook/role/__init__.py
@@ -27,6 +27,7 @@ from ansible.module_utils.common._collections_compat import Container, Mapping,
from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
from ansible.playbook.become import Become
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.conditional import Conditional
from ansible.playbook.helpers import load_list_of_blocks
from ansible.playbook.role.metadata import RoleMetadata
@@ -91,7 +92,7 @@ def hash_params(params):
return frozenset((params,))
-class Role(Base, Become, Conditional, Taggable):
+class Role(Base, Become, Conditional, Taggable, CollectionSearch):
_delegate_to = FieldAttribute(isa='string')
_delegate_facts = FieldAttribute(isa='bool')
@@ -99,6 +100,7 @@ class Role(Base, Become, Conditional, Taggable):
def __init__(self, play=None, from_files=None, from_include=False):
self._role_name = None
self._role_path = None
+ self._role_collection = None
self._role_params = dict()
self._loader = None
@@ -166,6 +168,7 @@ class Role(Base, Become, Conditional, Taggable):
if role_include.role not in play.ROLE_CACHE:
play.ROLE_CACHE[role_include.role] = dict()
+ # FIXME: how to handle cache keys for collection-based roles, since they're technically adjustable per task?
play.ROLE_CACHE[role_include.role][hashed_params] = r
return r
@@ -176,6 +179,7 @@ class Role(Base, Become, Conditional, Taggable):
def _load_role_data(self, role_include, parent_role=None):
self._role_name = role_include.role
self._role_path = role_include.get_role_path()
+ self._role_collection = role_include._role_collection
self._role_params = role_include.get_role_params()
self._variable_manager = role_include.get_variable_manager()
self._loader = role_include.get_loader()
@@ -194,9 +198,6 @@ class Role(Base, Become, Conditional, Taggable):
else:
self._attributes[attr_name] = role_include._attributes[attr_name]
- # ensure all plugins dirs for this role are added to plugin search path
- add_all_plugin_dirs(self._role_path)
-
# vars and default vars are regular dictionaries
self._role_vars = self._load_role_yaml('vars', main=self._from_files.get('vars'), allow_dir=True)
if self._role_vars is None:
@@ -218,6 +219,29 @@ class Role(Base, Become, Conditional, Taggable):
else:
self._metadata = RoleMetadata()
+ # reset collections list; roles do not inherit collections from parents, just use the defaults
+ # FUTURE: use a private config default for this so we can allow it to be overridden later
+ self.collections = []
+
+ # 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:
+ self.collections.insert(0, self._role_collection)
+ else:
+ # 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)
+
+ # 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'
+ if 'ansible.builtin' not in self.collections and 'ansible.legacy' not in self.collections:
+ self.collections.append(default_append_collection)
+
task_data = self._load_role_yaml('tasks', main=self._from_files.get('tasks'))
if task_data:
try:
diff --git a/lib/ansible/playbook/role/definition.py b/lib/ansible/playbook/role/definition.py
index 235a54490e..c5999bb1b3 100644
--- a/lib/ansible/playbook/role/definition.py
+++ b/lib/ansible/playbook/role/definition.py
@@ -28,9 +28,11 @@ from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleMapping
from ansible.playbook.attribute import Attribute, FieldAttribute
from ansible.playbook.base import Base
from ansible.playbook.become import Become
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.conditional import Conditional
from ansible.playbook.taggable import Taggable
from ansible.template import Templar
+from ansible.utils.collection_loader import get_collection_role_path, is_collection_ref
from ansible.utils.path import unfrackpath
from ansible.utils.display import Display
@@ -39,11 +41,11 @@ __all__ = ['RoleDefinition']
display = Display()
-class RoleDefinition(Base, Become, Conditional, Taggable):
+class RoleDefinition(Base, Become, Conditional, Taggable, CollectionSearch):
_role = FieldAttribute(isa='string')
- def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None):
+ def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None, collection_list=None):
super(RoleDefinition, self).__init__()
@@ -52,8 +54,10 @@ class RoleDefinition(Base, Become, Conditional, Taggable):
self._loader = loader
self._role_path = None
+ self._role_collection = None
self._role_basedir = role_basedir
self._role_params = dict()
+ self._collection_list = collection_list
# def __repr__(self):
# return 'ROLEDEF: ' + self._attributes.get('role', '<no name set>')
@@ -139,6 +143,31 @@ class RoleDefinition(Base, Become, Conditional, Taggable):
append it to the default role path
'''
+ # create a templar class to template the dependency names, in
+ # case they contain variables
+ if self._variable_manager is not None:
+ all_vars = self._variable_manager.get_vars(play=self._play)
+ else:
+ all_vars = dict()
+
+ templar = Templar(loader=self._loader, variables=all_vars)
+ role_name = templar.template(role_name)
+
+ role_tuple = None
+
+ # try to load as a collection-based role first
+ if self._collection_list or is_collection_ref(role_name):
+ role_tuple = get_collection_role_path(role_name, self._collection_list)
+
+ if role_tuple:
+ # we found it, stash collection data and return the name/path tuple
+ self._role_collection = role_tuple[2]
+ return role_tuple[0:2]
+
+ # FUTURE: refactor this to be callable from internal so we can properly order ansible.legacy searches with the collections keyword
+ if self._collection_list and 'ansible.legacy' not in self._collection_list:
+ raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(self._collection_list)), obj=self._ds)
+
# we always start the search for roles in the base directory of the playbook
role_search_paths = [
os.path.join(self._loader.get_basedir(), u'roles'),
@@ -158,16 +187,6 @@ class RoleDefinition(Base, Become, Conditional, Taggable):
# the roles/ dir appended
role_search_paths.append(self._loader.get_basedir())
- # create a templar class to template the dependency names, in
- # case they contain variables
- if self._variable_manager is not None:
- all_vars = self._variable_manager.get_vars(play=self._play)
- else:
- all_vars = dict()
-
- templar = Templar(loader=self._loader, variables=all_vars)
- role_name = templar.template(role_name)
-
# now iterate through the possible paths and return the first one we find
for path in role_search_paths:
path = templar.template(path)
diff --git a/lib/ansible/playbook/role/include.py b/lib/ansible/playbook/role/include.py
index ddcdf80997..1e5d901d96 100644
--- a/lib/ansible/playbook/role/include.py
+++ b/lib/ansible/playbook/role/include.py
@@ -43,11 +43,12 @@ class RoleInclude(RoleDefinition):
_delegate_to = FieldAttribute(isa='string')
_delegate_facts = FieldAttribute(isa='bool', default=False)
- def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None):
- super(RoleInclude, self).__init__(play=play, role_basedir=role_basedir, variable_manager=variable_manager, loader=loader)
+ def __init__(self, play=None, role_basedir=None, variable_manager=None, loader=None, collection_list=None):
+ super(RoleInclude, self).__init__(play=play, role_basedir=role_basedir, variable_manager=variable_manager,
+ loader=loader, collection_list=collection_list)
@staticmethod
- def load(data, play, current_role_path=None, parent_role=None, variable_manager=None, loader=None):
+ def load(data, play, current_role_path=None, parent_role=None, variable_manager=None, loader=None, collection_list=None):
if not (isinstance(data, string_types) or isinstance(data, dict) or isinstance(data, AnsibleBaseYAMLObject)):
raise AnsibleParserError("Invalid role definition: %s" % to_native(data))
@@ -55,5 +56,5 @@ class RoleInclude(RoleDefinition):
if isinstance(data, string_types) and ',' in data:
raise AnsibleError("Invalid old style role requirement: %s" % data)
- ri = RoleInclude(play=play, role_basedir=current_role_path, variable_manager=variable_manager, loader=loader)
+ ri = RoleInclude(play=play, role_basedir=current_role_path, variable_manager=variable_manager, loader=loader, collection_list=collection_list)
return ri.load_data(data, variable_manager=variable_manager, loader=loader)
diff --git a/lib/ansible/playbook/role/metadata.py b/lib/ansible/playbook/role/metadata.py
index b50387329c..fd1d873734 100644
--- a/lib/ansible/playbook/role/metadata.py
+++ b/lib/ansible/playbook/role/metadata.py
@@ -23,17 +23,17 @@ import os
from ansible.errors import AnsibleParserError, AnsibleError
from ansible.module_utils._text import to_native
-from ansible.module_utils.six import iteritems, string_types
-from ansible.playbook.attribute import Attribute, FieldAttribute
+from ansible.module_utils.six import string_types
+from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.helpers import load_list_of_roles
-from ansible.playbook.role.include import RoleInclude
from ansible.playbook.role.requirement import RoleRequirement
__all__ = ['RoleMetadata']
-class RoleMetadata(Base):
+class RoleMetadata(Base, CollectionSearch):
'''
This class wraps the parsing and validation of the optional metadata
within each Role (meta/main.yml).
@@ -105,7 +105,7 @@ class RoleMetadata(Base):
def serialize(self):
return dict(
allow_duplicates=self._allow_duplicates,
- dependencies=self._dependencies,
+ dependencies=self._dependencies
)
def deserialize(self, data):
diff --git a/lib/ansible/playbook/role_include.py b/lib/ansible/playbook/role_include.py
index e63c4ac537..870dea80d6 100644
--- a/lib/ansible/playbook/role_include.py
+++ b/lib/ansible/playbook/role_include.py
@@ -73,7 +73,7 @@ class IncludeRole(TaskInclude):
else:
myplay = play
- ri = RoleInclude.load(self._role_name, play=myplay, variable_manager=variable_manager, loader=loader)
+ ri = RoleInclude.load(self._role_name, play=myplay, variable_manager=variable_manager, loader=loader, collection_list=self.collections)
ri.vars.update(self.vars)
# build role
@@ -97,9 +97,14 @@ class IncludeRole(TaskInclude):
p_block = self.build_parent_block()
+ # collections value is not inherited; override with the value we calculated during role setup
+ p_block.collections = actual_role.collections
+
blocks = actual_role.compile(play=myplay, dep_chain=dep_chain)
for b in blocks:
b._parent = p_block
+ # HACK: parent inheritance doesn't seem to have a way to handle this intermediate override until squashed/finalized
+ b.collections = actual_role.collections
# updated available handlers in play
handlers = actual_role.get_handler_blocks(play=myplay)
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
index 80854f3fb9..68204b4968 100644
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -32,6 +32,7 @@ from ansible.playbook.attribute import FieldAttribute
from ansible.playbook.base import Base
from ansible.playbook.become import Become
from ansible.playbook.block import Block
+from ansible.playbook.collectionsearch import CollectionSearch
from ansible.playbook.conditional import Conditional
from ansible.playbook.loop_control import LoopControl
from ansible.playbook.role import Role
@@ -44,7 +45,7 @@ __all__ = ['Task']
display = Display()
-class Task(Base, Conditional, Taggable, Become):
+class Task(Base, Conditional, Taggable, Become, CollectionSearch):
"""
A task is a language feature that represents a call to a module, with given arguments and other parameters.
@@ -180,7 +181,7 @@ class Task(Base, Conditional, Taggable, Become):
# 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)
+ args_parser = ModuleArgsParser(task_ds=ds, collection_list=self.collections)
try:
(action, args, delegate_to) = args_parser.parse()
except AnsibleParserError as e: