Example #1
0
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)
Example #2
0
    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])
Example #3
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
Example #4
0
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
Example #5
0
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))
Example #6
0
    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)
Example #7
0
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
Example #9
0
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)
Example #10
0
    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)
Example #11
0
    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
Example #12
0
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
Example #13
0
    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,
        )
Example #14
0
def test_fqcn_validation(fqcn, expected):
    """Vefiry that is_valid_collection_name validates FQCN correctly."""
    assert AnsibleCollectionRef.is_valid_collection_name(fqcn) is expected
Example #15
0
    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
Example #16
0
    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)
Example #17
0
    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)
Example #18
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)