diff options
author | Matt Davis <nitzmahone@users.noreply.github.com> | 2019-03-28 10:41:39 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-03-28 10:41:39 -0700 |
commit | f86345f777b8c592c83dd6ef5928ee0ca092a063 (patch) | |
tree | 18cd9df7db60d2138edb29c5d0bcb47a3647efa9 /lib/ansible/playbook | |
parent | 5173548a9fb53843ed99a283c8c8b21ea9710418 (diff) | |
download | ansible-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__.py | 2 | ||||
-rw-r--r-- | lib/ansible/playbook/block.py | 3 | ||||
-rw-r--r-- | lib/ansible/playbook/collectionsearch.py | 26 | ||||
-rw-r--r-- | lib/ansible/playbook/helpers.py | 8 | ||||
-rw-r--r-- | lib/ansible/playbook/play.py | 3 | ||||
-rw-r--r-- | lib/ansible/playbook/role/__init__.py | 32 | ||||
-rw-r--r-- | lib/ansible/playbook/role/definition.py | 43 | ||||
-rw-r--r-- | lib/ansible/playbook/role/include.py | 9 | ||||
-rw-r--r-- | lib/ansible/playbook/role/metadata.py | 10 | ||||
-rw-r--r-- | lib/ansible/playbook/role_include.py | 7 | ||||
-rw-r--r-- | lib/ansible/playbook/task.py | 5 |
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: |