def test_legacy_plugin_dir_to_plugin_type(dirname, expected_result): if isinstance(expected_result, string_types): assert AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type( dirname) == expected_result else: with pytest.raises(expected_result): AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type(dirname)
def _find_fq_plugin(self, fq_name, extension): """Search builtin paths to find a plugin. No external paths are searched, meaning plugins inside roles inside collections will be ignored. """ plugin_type = AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type( self.subdir) acr = AnsibleCollectionRef.from_fqcr(fq_name, plugin_type) n_resource = to_native(acr.resource, errors='strict') # we want this before the extension is added full_name = '{0}.{1}'.format(acr.n_python_package_name, n_resource) if extension: n_resource += extension pkg = sys.modules.get(acr.n_python_package_name) if not pkg: # FIXME: there must be cheaper/safer way to do this pkg = import_module(acr.n_python_package_name) # if the package is one of our flatmaps, we need to consult its loader to find the path, since the file could be # anywhere in the tree if hasattr(pkg, '__loader__') and isinstance(pkg.__loader__, AnsibleFlatMapLoader): try: file_path = pkg.__loader__.find_file(n_resource) return full_name, to_text(file_path) except IOError: # this loader already takes care of extensionless files, so if we didn't find it, just bail return None, None pkg_path = os.path.dirname(pkg.__file__) n_resource_path = os.path.join(pkg_path, n_resource) # FIXME: and is file or file link or ... if os.path.exists(n_resource_path): return full_name, to_text(n_resource_path) # look for any matching extension in the package location (sans filter) ext_blacklist = ['.pyc', '.pyo'] found_files = [ f for f in glob.iglob(os.path.join(pkg_path, n_resource) + '.*') if os.path.isfile(f) and os.path.splitext(f)[1] not in ext_blacklist ] if not found_files: return None, None if len(found_files) > 1: # TODO: warn? pass return full_name, to_text(found_files[0])
def test_fqcr_parsing_invalid(ref, ref_type, expected_error_type, expected_error_expression): assert not AnsibleCollectionRef.is_valid_fqcr(ref, ref_type) with pytest.raises(expected_error_type) as curerr: AnsibleCollectionRef.from_fqcr(ref, ref_type) assert re.search(expected_error_expression, str(curerr.value)) r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type) assert r is None
def test_fqcr_parsing_valid(ref, ref_type, expected_collection, expected_subdirs, expected_resource, expected_python_pkg_name): assert AnsibleCollectionRef.is_valid_fqcr(ref, ref_type) r = AnsibleCollectionRef.from_fqcr(ref, ref_type) assert r.collection == expected_collection assert r.subdirs == expected_subdirs assert r.resource == expected_resource assert r.n_python_package_name == expected_python_pkg_name r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type) assert r.collection == expected_collection assert r.subdirs == expected_subdirs assert r.resource == expected_resource assert r.n_python_package_name == expected_python_pkg_name
def test_collectionref_components_invalid(name, subdirs, resource, ref_type, expected_error_type, expected_error_expression): with pytest.raises(expected_error_type) as curerr: AnsibleCollectionRef(name, subdirs, resource, ref_type) assert re.search(expected_error_expression, str(curerr.value))
def find_plugin(self, name, mod_type='', ignore_deprecated=False, check_aliases=False, collection_list=None): ''' Find a plugin named name ''' global _PLUGIN_FILTERS if name in _PLUGIN_FILTERS[self.package]: return None if mod_type: suffix = mod_type elif self.class_name: # Ansible plugins that run in the controller process (most plugins) suffix = '.py' else: # Only Ansible Modules. Ansible modules can be any executable so # they can have any suffix suffix = '' # FIXME: need this right now so we can still load shipped PS module_utils- come up with a more robust solution if (AnsibleCollectionRef.is_valid_fqcr(name) or collection_list) and not name.startswith('Ansible'): if '.' in name or not collection_list: candidates = [name] else: candidates = [ '{0}.{1}'.format(c, name) for c in collection_list ] # TODO: keep actual errors, not just assembled messages errors = [] for candidate_name in candidates: try: # HACK: refactor this properly if candidate_name.startswith('ansible.legacy'): # just pass the raw name to the old lookup function to check in all the usual locations p = self._find_plugin_legacy( name.replace('ansible.legacy.', '', 1), ignore_deprecated, check_aliases, suffix) else: p = self._find_fq_plugin(candidate_name, suffix) if p: return p except Exception as ex: errors.append(to_native(ex)) if errors: display.debug( msg='plugin lookup for {0} failed; errors: {1}'.format( name, '; '.join(errors))) return None # if we got here, there's no collection list and it's not an FQ name, so do legacy lookup return self._find_plugin_legacy(name, ignore_deprecated, check_aliases, suffix)
def test_collectionref_components_valid(name, subdirs, resource, ref_type, python_pkg_name): x = AnsibleCollectionRef(name, subdirs, resource, ref_type) assert x.collection == name if subdirs: assert x.subdirs == subdirs else: assert x.subdirs == '' assert x.resource == resource assert x.ref_type == ref_type assert x.n_python_package_name == python_pkg_name
def load_callbacks(self): ''' Loads all available callbacks, with the exception of those which utilize the CALLBACK_TYPE option. When CALLBACK_TYPE is set to 'stdout', only one such callback plugin will be loaded. ''' if self._callbacks_loaded: return stdout_callback_loaded = False if self._stdout_callback is None: self._stdout_callback = C.DEFAULT_STDOUT_CALLBACK if isinstance(self._stdout_callback, CallbackBase): stdout_callback_loaded = True elif isinstance(self._stdout_callback, string_types): if self._stdout_callback not in callback_loader: raise AnsibleError("Invalid callback for stdout specified: %s" % self._stdout_callback) else: self._stdout_callback = callback_loader.get(self._stdout_callback) self._stdout_callback.set_options() stdout_callback_loaded = True else: raise AnsibleError("callback must be an instance of CallbackBase or the name of a callback plugin") for callback_plugin in callback_loader.all(class_only=True): callback_type = getattr(callback_plugin, 'CALLBACK_TYPE', '') callback_needs_whitelist = getattr(callback_plugin, 'CALLBACK_NEEDS_WHITELIST', False) (callback_name, _) = os.path.splitext(os.path.basename(callback_plugin._original_path)) if callback_type == 'stdout': # we only allow one callback of type 'stdout' to be loaded, if callback_name != self._stdout_callback or stdout_callback_loaded: continue stdout_callback_loaded = True elif callback_name == 'tree' and self._run_tree: # special case for ansible cli option pass elif not self._run_additional_callbacks or (callback_needs_whitelist and ( C.DEFAULT_CALLBACK_WHITELIST is None or callback_name not in C.DEFAULT_CALLBACK_WHITELIST)): # 2.x plugins shipped with ansible should require whitelisting, older or non shipped should load automatically continue callback_obj = callback_plugin() callback_obj.set_options() self._callback_plugins.append(callback_obj) for callback_plugin_name in (c for c in C.DEFAULT_CALLBACK_WHITELIST if AnsibleCollectionRef.is_valid_fqcr(c)): # TODO: need to extend/duplicate the stdout callback check here (and possible move this ahead of the old way callback_obj = callback_loader.get(callback_plugin_name) self._callback_plugins.append(callback_obj) self._callbacks_loaded = True
def validate_collection_name(name): """ Validates the collection name as an input from the user or a requirements file fit the requirements. :param name: The input name with optional range specifier split by ':'. :return: The input value, required for argparse validation. """ collection, dummy, dummy = name.partition(':') if AnsibleCollectionRef.is_valid_collection_name(collection): return name raise AnsibleError("Invalid collection name '%s', name must be in the format <namespace>.<collection>." % name)
def find_plugin_with_name(self, name, mod_type='', ignore_deprecated=False, check_aliases=False, collection_list=None): ''' Find a plugin named name ''' global _PLUGIN_FILTERS if name in _PLUGIN_FILTERS[self.package]: return None, None if mod_type: suffix = mod_type elif self.class_name: # Ansible plugins that run in the controller process (most plugins) suffix = '.py' else: # Only Ansible Modules. Ansible modules can be any executable so # they can have any suffix suffix = '' # FIXME: need this right now so we can still load shipped PS module_utils- come up with a more robust solution if (AnsibleCollectionRef.is_valid_fqcr(name) or collection_list) and not name.startswith('Ansible'): if '.' in name or not collection_list: candidates = [name] else: candidates = ['{0}.{1}'.format(c, name) for c in collection_list] # TODO: keep actual errors, not just assembled messages errors = [] for candidate_name in candidates: try: # HACK: refactor this properly if candidate_name.startswith('ansible.legacy'): # 'ansible.legacy' refers to the plugin finding behavior used before collections existed. # They need to search 'library' and the various '*_plugins' directories in order to find the file. full_name = name p = self._find_plugin_legacy(name.replace('ansible.legacy.', '', 1), ignore_deprecated, check_aliases, suffix) else: # 'ansible.builtin' should be handled here. This means only internal, or builtin, paths are searched. full_name, p = self._find_fq_plugin(candidate_name, suffix) if p: return full_name, p except Exception as ex: errors.append(to_native(ex)) if errors: display.debug(msg='plugin lookup for {0} failed; errors: {1}'.format(name, '; '.join(errors))) return None, None # if we got here, there's no collection list and it's not an FQ name, so do legacy lookup return name, self._find_plugin_legacy(name, ignore_deprecated, check_aliases, suffix)
def __getitem__(self, key): if not isinstance(key, string_types): raise ValueError('key must be a string') key = to_native(key) if '.' not in key: # might be a built-in value, delegate to base dict return self._delegatee.__getitem__(key) func = self._collection_jinja_func_cache.get(key) if func: return func acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname) if not acr: raise KeyError('invalid plugin name: {0}'.format(key)) try: pkg = import_module(acr.n_python_package_name) except ImportError: raise KeyError() parent_prefix = acr.collection if acr.subdirs: parent_prefix = '{0}.{1}'.format(parent_prefix, acr.subdirs) for dummy, module_name, ispkg in pkgutil.iter_modules( pkg.__path__, prefix=parent_prefix + '.'): if ispkg: continue try: plugin_impl = self._pluginloader.get(module_name) except Exception as e: raise TemplateSyntaxError(to_native(e), 0) method_map = getattr(plugin_impl, self._method_map_name) for f in iteritems(method_map()): fq_name = '.'.join((parent_prefix, f[0])) # FIXME: detect/warn on intra-collection function name collisions self._collection_jinja_func_cache[fq_name] = f[1] function_impl = self._collection_jinja_func_cache[key] return function_impl
def get_vars_from_path(loader, path, entities, stage): data = {} vars_plugin_list = list(vars_loader.all()) for plugin_name in C.VARIABLE_PLUGINS_ENABLED: if AnsibleCollectionRef.is_valid_fqcr(plugin_name): vars_plugin = vars_loader.get(plugin_name) if vars_plugin is None: # Error if there's no play directory or the name is wrong? continue if vars_plugin not in vars_plugin_list: vars_plugin_list.append(vars_plugin) for plugin in vars_plugin_list: if plugin._load_name not in C.VARIABLE_PLUGINS_ENABLED and getattr( plugin, 'REQUIRES_WHITELIST', False): # 2.x plugins shipped with ansible should require enabling, older or non shipped should load automatically continue has_stage = hasattr(plugin, 'get_option') and plugin.has_option('stage') # if a plugin-specific setting has not been provided, use the global setting # older/non shipped plugins that don't support the plugin-specific setting should also use the global setting use_global = (has_stage and plugin.get_option('stage') is None) or not has_stage if use_global: if C.RUN_VARS_PLUGINS == 'demand' and stage == 'inventory': continue elif C.RUN_VARS_PLUGINS == 'start' and stage == 'task': continue elif has_stage and plugin.get_option('stage') not in ('all', stage): continue data = combine_vars(data, get_plugin_vars(loader, plugin, path, entities)) return data
def from_requirement_dict(cls, collection_req, art_mgr, validate_signature_options=True): req_name = collection_req.get('name', None) req_version = collection_req.get('version', '*') req_type = collection_req.get('type') # TODO: decide how to deprecate the old src API behavior req_source = collection_req.get('source', None) req_signature_sources = collection_req.get('signatures', None) if req_signature_sources is not None: if validate_signature_options and art_mgr.keyring is None: raise AnsibleError( f"Signatures were provided to verify {req_name} but no keyring was configured." ) if not isinstance(req_signature_sources, MutableSequence): req_signature_sources = [req_signature_sources] req_signature_sources = frozenset(req_signature_sources) if req_type is None: if ( # FIXME: decide on the future behavior: _ALLOW_CONCRETE_POINTER_IN_SOURCE and req_source is not None and _is_concrete_artifact_pointer(req_source)): src_path = req_source elif (req_name is not None and AnsibleCollectionRef.is_valid_collection_name(req_name)): req_type = 'galaxy' elif (req_name is not None and _is_concrete_artifact_pointer(req_name)): src_path, req_name = req_name, None else: dir_tip_tmpl = ( # NOTE: leading LFs are for concat '\n\nTip: Make sure you are pointing to the right ' 'subdirectory — `{src!s}` looks like a directory ' 'but it is neither a collection, nor a namespace ' 'dir.') if req_source is not None and os.path.isdir(req_source): tip = dir_tip_tmpl.format(src=req_source) elif req_name is not None and os.path.isdir(req_name): tip = dir_tip_tmpl.format(src=req_name) elif req_name: tip = '\n\nCould not find {0}.'.format(req_name) else: tip = '' raise AnsibleError( # NOTE: I'd prefer a ValueError instead 'Neither the collection requirement entry key ' "'name', nor 'source' point to a concrete " "resolvable collection artifact. Also 'name' is " 'not an FQCN. A valid collection name must be in ' 'the format <namespace>.<collection>. Please make ' 'sure that the namespace and the collection name ' ' contain characters from [a-zA-Z0-9_] only.' '{extra_tip!s}'.format(extra_tip=tip), ) if req_type is None: if _is_git_url(src_path): req_type = 'git' req_source = src_path elif _is_http_url(src_path): req_type = 'url' req_source = src_path elif _is_file_path(src_path): req_type = 'file' req_source = src_path elif _is_collection_dir(src_path): if _is_installed_collection_dir( src_path) and _is_collection_src_dir(src_path): # Note that ``download`` requires a dir with a ``galaxy.yml`` and fails if it # doesn't exist, but if a ``MANIFEST.json`` also exists, it would be used # instead of the ``galaxy.yml``. raise AnsibleError( u"Collection requirement at '{path!s}' has both a {manifest_json!s} " u"file and a {galaxy_yml!s}.\nThe requirement must either be an installed " u"collection directory or a source collection directory, not both." .format( path=to_text(src_path, errors='surrogate_or_strict'), manifest_json=to_text(_MANIFEST_JSON), galaxy_yml=to_text(_GALAXY_YAML), )) req_type = 'dir' req_source = src_path elif _is_collection_namespace_dir(src_path): req_name = None # No name for a virtual req or "namespace."? req_type = 'subdirs' req_source = src_path else: raise AnsibleError( # NOTE: this is never supposed to be hit 'Failed to automatically detect the collection ' 'requirement type.', ) if req_type not in {'file', 'galaxy', 'git', 'url', 'dir', 'subdirs'}: raise AnsibleError( "The collection requirement entry key 'type' must be " 'one of file, galaxy, git, dir, subdirs, or url.') if req_name is None and req_type == 'galaxy': raise AnsibleError( 'Collections requirement entry should contain ' "the key 'name' if it's requested from a Galaxy-like " 'index server.', ) if req_type != 'galaxy' and req_source is None: req_source, req_name = req_name, None if (req_type == 'galaxy' and isinstance(req_source, GalaxyAPI) and not _is_http_url(req_source.api_server)): raise AnsibleError( "Collections requirement 'source' entry should contain " 'a valid Galaxy API URL but it does not: {not_url!s} ' 'is not an HTTP URL.'.format(not_url=req_source.api_server), ) tmp_inst_req = cls(req_name, req_version, req_source, req_type, req_signature_sources) if req_type not in {'galaxy', 'subdirs'} and req_name is None: req_name = art_mgr.get_direct_collection_fqcn( tmp_inst_req) # TODO: fix the cache key in artifacts manager? if req_type not in {'galaxy', 'subdirs'} and req_version == '*': req_version = art_mgr.get_direct_collection_version(tmp_inst_req) return cls( req_name, req_version, req_source, req_type, req_signature_sources, )
def test_fqcn_validation(fqcn, expected): """Vefiry that is_valid_collection_name validates FQCN correctly.""" assert AnsibleCollectionRef.is_valid_collection_name(fqcn) is expected
def __getitem__(self, key): try: if not isinstance(key, string_types): raise ValueError('key must be a string') key = to_native(key) if '.' not in key: # might be a built-in or legacy, check the delegatee dict first, then try for a last-chance base redirect func = self._delegatee.get(key) if func: return func ts = _get_collection_metadata('ansible.builtin') # TODO: implement support for collection-backed redirect (currently only builtin) # TODO: implement cycle detection (unified across collection redir as well) redirect_fqcr = ts.get('plugin_routing', {}).get(self._dirname, {}).get(key, {}).get('redirect', None) if redirect_fqcr: acr = AnsibleCollectionRef.from_fqcr(ref=redirect_fqcr, ref_type=self._dirname) display.vvv('redirecting {0} {1} to {2}.{3}'.format(self._dirname, key, acr.collection, acr.resource)) key = redirect_fqcr # TODO: handle recursive forwarding (not necessary for builtin, but definitely for further collection redirs) func = self._collection_jinja_func_cache.get(key) if func: return func acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname) if not acr: raise KeyError('invalid plugin name: {0}'.format(key)) try: pkg = import_module(acr.n_python_package_name) except ImportError: raise KeyError() parent_prefix = acr.collection if acr.subdirs: parent_prefix = '{0}.{1}'.format(parent_prefix, acr.subdirs) # TODO: implement collection-level redirect for dummy, module_name, ispkg in pkgutil.iter_modules(pkg.__path__, prefix=parent_prefix + '.'): if ispkg: continue try: plugin_impl = self._pluginloader.get(module_name) except Exception as e: raise TemplateSyntaxError(to_native(e), 0) method_map = getattr(plugin_impl, self._method_map_name) for f in iteritems(method_map()): fq_name = '.'.join((parent_prefix, f[0])) # FIXME: detect/warn on intra-collection function name collisions self._collection_jinja_func_cache[fq_name] = f[1] function_impl = self._collection_jinja_func_cache[key] return function_impl except KeyError: raise except Exception as ex: display.warning('an unexpected error occurred during Jinja2 environment setup: {0}'.format(to_native(ex))) display.vvv('exception during Jinja2 environment setup: {0}'.format(format_exc())) raise
def _load_role_path(self, role_name): ''' the 'role', as specified in the ds (or as a bare string), can either be a simple name or a full path. If it is a full path, we use the basename as the role name, otherwise we take the name as-given and 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 = {} 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 AnsibleCollectionRef.is_valid_fqcr( 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] # We didn't find a collection role, look in defined role paths # FUTURE: refactor this to be callable from internal so we can properly order # ansible.legacy searches with the collections keyword # 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'), ] # also search in the configured roles path if C.DEFAULT_ROLES_PATH: role_search_paths.extend(C.DEFAULT_ROLES_PATH) # next, append the roles basedir, if it was set, so we can # search relative to that directory for dependent roles if self._role_basedir: role_search_paths.append(self._role_basedir) # finally as a last resort we look in the current basedir as set # in the loader (which should be the playbook dir itself) but without # the roles/ dir appended role_search_paths.append(self._loader.get_basedir()) # now iterate through the possible paths and return the first one we find for path in role_search_paths: path = templar.template(path) role_path = unfrackpath(os.path.join(path, role_name)) if self._loader.path_exists(role_path): return (role_name, role_path) # if not found elsewhere try to extract path from name role_path = unfrackpath(role_name) if self._loader.path_exists(role_path): role_name = os.path.basename(role_name) return (role_name, role_path) searches = (self._collection_list or []) + role_search_paths raise AnsibleError("the role '%s' was not found in %s" % (role_name, ":".join(searches)), obj=self._ds)
def __getitem__(self, key): try: if not isinstance(key, string_types): raise ValueError('key must be a string') key = to_native(key) if '.' not in key: # might be a built-in or legacy, check the delegatee dict first, then try for a last-chance base redirect func = self._delegatee.get(key) if func: return func # didn't find it in the pre-built Jinja env, assume it's a former builtin and follow the normal routing path leaf_key = key key = 'ansible.builtin.' + key else: leaf_key = key.split('.')[-1] acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname) if not acr: raise KeyError('invalid plugin name: {0}'.format(key)) ts = _get_collection_metadata(acr.collection) # TODO: implement support for collection-backed redirect (currently only builtin) # TODO: implement cycle detection (unified across collection redir as well) routing_entry = ts.get('plugin_routing', {}).get(self._dirname, {}).get(leaf_key, {}) deprecation_entry = routing_entry.get('deprecation') if deprecation_entry: warning_text = deprecation_entry.get('warning_text') removal_date = deprecation_entry.get('removal_date') removal_version = deprecation_entry.get('removal_version') if not warning_text: warning_text = '{0} "{1}" is deprecated'.format(self._dirname, key) display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection) tombstone_entry = routing_entry.get('tombstone') if tombstone_entry: warning_text = tombstone_entry.get('warning_text') removal_date = tombstone_entry.get('removal_date') removal_version = tombstone_entry.get('removal_version') if not warning_text: warning_text = '{0} "{1}" has been removed'.format(self._dirname, key) exc_msg = display.get_deprecation_message(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection, removed=True) raise AnsiblePluginRemovedError(exc_msg) redirect_fqcr = routing_entry.get('redirect', None) if redirect_fqcr: acr = AnsibleCollectionRef.from_fqcr(ref=redirect_fqcr, ref_type=self._dirname) display.vvv('redirecting {0} {1} to {2}.{3}'.format(self._dirname, key, acr.collection, acr.resource)) key = redirect_fqcr # TODO: handle recursive forwarding (not necessary for builtin, but definitely for further collection redirs) func = self._collection_jinja_func_cache.get(key) if func: return func try: pkg = import_module(acr.n_python_package_name) except ImportError: raise KeyError() parent_prefix = acr.collection if acr.subdirs: parent_prefix = '{0}.{1}'.format(parent_prefix, acr.subdirs) # TODO: implement collection-level redirect for dummy, module_name, ispkg in pkgutil.iter_modules(pkg.__path__, prefix=parent_prefix + '.'): if ispkg: continue try: plugin_impl = self._pluginloader.get(module_name) except Exception as e: raise TemplateSyntaxError(to_native(e), 0) method_map = getattr(plugin_impl, self._method_map_name) for func_name, func in iteritems(method_map()): fq_name = '.'.join((parent_prefix, func_name)) # FIXME: detect/warn on intra-collection function name collisions if USE_JINJA2_NATIVE and func_name in C.STRING_TYPE_FILTERS: self._collection_jinja_func_cache[fq_name] = _wrap_native_text(func) else: self._collection_jinja_func_cache[fq_name] = _unroll_iterator(func) function_impl = self._collection_jinja_func_cache[key] return function_impl except AnsiblePluginRemovedError as apre: raise TemplateSyntaxError(to_native(apre), 0) except KeyError: raise except Exception as ex: display.warning('an unexpected error occurred during Jinja2 environment setup: {0}'.format(to_native(ex))) display.vvv('exception during Jinja2 environment setup: {0}'.format(format_exc())) raise TemplateSyntaxError(to_native(ex), 0)
def __getitem__(self, key): original_key = key self._load_ansible_plugins() try: if not isinstance(key, string_types): raise ValueError('key must be a string') key = to_native(key) if '.' not in key: # might be a built-in or legacy, check the delegatee dict first, then try for a last-chance base redirect func = self._delegatee.get(key) if func: return func key, leaf_key = get_fqcr_and_name(key) seen = set() while True: if key in seen: raise TemplateSyntaxError( 'recursive collection redirect found for %r' % original_key, 0) seen.add(key) acr = AnsibleCollectionRef.try_parse_fqcr(key, self._dirname) if not acr: raise KeyError('invalid plugin name: {0}'.format(key)) ts = _get_collection_metadata(acr.collection) # TODO: implement cycle detection (unified across collection redir as well) routing_entry = ts.get('plugin_routing', {}).get(self._dirname, {}).get(leaf_key, {}) deprecation_entry = routing_entry.get('deprecation') if deprecation_entry: warning_text = deprecation_entry.get('warning_text') removal_date = deprecation_entry.get('removal_date') removal_version = deprecation_entry.get('removal_version') if not warning_text: warning_text = '{0} "{1}" is deprecated'.format( self._dirname, key) display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection) tombstone_entry = routing_entry.get('tombstone') if tombstone_entry: warning_text = tombstone_entry.get('warning_text') removal_date = tombstone_entry.get('removal_date') removal_version = tombstone_entry.get('removal_version') if not warning_text: warning_text = '{0} "{1}" has been removed'.format( self._dirname, key) exc_msg = display.get_deprecation_message( warning_text, version=removal_version, date=removal_date, collection_name=acr.collection, removed=True) raise AnsiblePluginRemovedError(exc_msg) redirect = routing_entry.get('redirect', None) if redirect: next_key, leaf_key = get_fqcr_and_name( redirect, collection=acr.collection) display.vvv( 'redirecting (type: {0}) {1}.{2} to {3}'.format( self._dirname, acr.collection, acr.resource, next_key)) key = next_key else: break func = self._collection_jinja_func_cache.get(key) if func: return func try: pkg = import_module(acr.n_python_package_name) except ImportError: raise KeyError() parent_prefix = acr.collection if acr.subdirs: parent_prefix = '{0}.{1}'.format(parent_prefix, acr.subdirs) # TODO: implement collection-level redirect for dummy, module_name, ispkg in pkgutil.iter_modules( pkg.__path__, prefix=parent_prefix + '.'): if ispkg: continue try: plugin_impl = self._pluginloader.get(module_name) except Exception as e: raise TemplateSyntaxError(to_native(e), 0) try: method_map = getattr(plugin_impl, self._method_map_name) func_items = method_map().items() except Exception as e: display.warning( "Skipping %s plugin %s as it seems to be invalid: %r" % (self._dirname, to_text( plugin_impl._original_path), e), ) continue for func_name, func in func_items: fq_name = '.'.join((parent_prefix, func_name)) # FIXME: detect/warn on intra-collection function name collisions if self._pluginloader.class_name == 'FilterModule': if fq_name.startswith(('ansible.builtin.', 'ansible.legacy.')) and \ func_name in C.STRING_TYPE_FILTERS: self._collection_jinja_func_cache[ fq_name] = _wrap_native_text(func) else: self._collection_jinja_func_cache[ fq_name] = _unroll_iterator(func) else: self._collection_jinja_func_cache[fq_name] = func function_impl = self._collection_jinja_func_cache[key] return function_impl except AnsiblePluginRemovedError as apre: raise TemplateSyntaxError(to_native(apre), 0) except KeyError: raise except Exception as ex: display.warning( 'an unexpected error occurred during Jinja2 environment setup: {0}' .format(to_native(ex))) display.vvv( 'exception during Jinja2 environment setup: {0}'.format( format_exc())) raise TemplateSyntaxError(to_native(ex), 0)