def test_on_collection_load(): finder = get_default_finder() reset_collections_loader_state(finder) load_handler = MagicMock() AnsibleCollectionConfig.on_collection_load += load_handler m = import_module('ansible_collections.testns.testcoll') load_handler.assert_called_once_with(collection_name='testns.testcoll', collection_path=os.path.dirname(m.__file__)) _meta = _get_collection_metadata('testns.testcoll') assert _meta # FIXME: compare to disk finder = get_default_finder() reset_collections_loader_state(finder) AnsibleCollectionConfig.on_collection_load += MagicMock(side_effect=Exception('bang')) with pytest.raises(Exception) as ex: import_module('ansible_collections.testns.testcoll') assert 'bang' in str(ex.value)
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 _resolve_group(self, fq_group_name, mandatory=True): if not AnsibleCollectionRef.is_valid_fqcr(fq_group_name): collection_name = 'ansible.builtin' fq_group_name = collection_name + '.' + fq_group_name else: collection_name = '.'.join(fq_group_name.split('.')[0:2]) # Check if the group has already been resolved and cached if fq_group_name in self.play._group_actions: return fq_group_name, self.play._group_actions[fq_group_name] try: action_groups = _get_collection_metadata(collection_name).get('action_groups', {}) except ValueError: if not mandatory: display.vvvvv("Error loading module_defaults: could not resolve the module_defaults group %s" % fq_group_name) return fq_group_name, [] raise AnsibleParserError("Error loading module_defaults: could not resolve the module_defaults group %s" % fq_group_name) # The collection may or may not use the fully qualified name # Don't fail if the group doesn't exist in the collection resource_name = fq_group_name.split(collection_name + '.')[-1] action_group = action_groups.get( fq_group_name, action_groups.get(resource_name) ) if action_group is None: if not mandatory: display.vvvvv("Error loading module_defaults: could not resolve the module_defaults group %s" % fq_group_name) return fq_group_name, [] raise AnsibleParserError("Error loading module_defaults: could not resolve the module_defaults group %s" % fq_group_name) resolved_actions = [] include_groups = [] found_group_metadata = False for action in action_group: # Everything should be a string except the metadata entry if not isinstance(action, string_types): _validate_action_group_metadata(action, found_group_metadata, fq_group_name) if isinstance(action['metadata'], dict): found_group_metadata = True include_groups = action['metadata'].get('extend_group', []) if isinstance(include_groups, string_types): include_groups = [include_groups] if not isinstance(include_groups, list): # Bad entries may be a warning above, but prevent tracebacks by setting it back to the acceptable type. include_groups = [] continue # The collection may or may not use the fully qualified name. # If not, it's part of the current collection. if not AnsibleCollectionRef.is_valid_fqcr(action): action = collection_name + '.' + action resolved_action = self._resolve_action(action, mandatory=False) if resolved_action: resolved_actions.append(resolved_action) for action in resolved_actions: if action not in self.play._action_groups: self.play._action_groups[action] = [] self.play._action_groups[action].append(fq_group_name) self.play._group_actions[fq_group_name] = resolved_actions # Resolve extended groups last, after caching the group in case they recursively refer to each other for include_group in include_groups: if not AnsibleCollectionRef.is_valid_fqcr(include_group): include_group_collection = collection_name include_group = collection_name + '.' + include_group else: include_group_collection = '.'.join(include_group.split('.')[0:2]) dummy, group_actions = self._resolve_group(include_group, mandatory=False) for action in group_actions: if action not in self.play._action_groups: self.play._action_groups[action] = [] self.play._action_groups[action].append(fq_group_name) self.play._group_actions[fq_group_name].extend(group_actions) resolved_actions.extend(group_actions) return fq_group_name, resolved_actions
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)
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