# -*- coding: utf-8 -*- # Copyright (c) 2015-2021, Felix Fontein # Copyright (c) 2018 Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = """ name: dependent short_description: Composes a list with nested elements of other lists or dicts which can depend on previous loop variables author: Felix Fontein (@felixfontein) version_added: 3.1.0 description: - "Takes the input lists and returns a list with elements that are lists, dictionaries, or template expressions which evaluate to lists or dicts, composed of the elements of the input evaluated lists and dictionaries." options: _raw: description: - A list where the elements are one-element dictionaries, mapping a name to a string, list, or dictionary. The name is the index that is used in the result object. The value is iterated over as described below. - If the value is a list, it is simply iterated over. - If the value is a dictionary, it is iterated over and returned as if they would be processed by the R(ansible.builtin.dict2items filter,ansible_collections.ansible.builtin.dict2items_filter). - If the value is a string, it is evaluated as Jinja2 expressions which can access the previously chosen elements with C(item.). The result must be a list or a dictionary. type: list elements: dict required: true """ EXAMPLES = """ - name: Install/remove public keys for active admin users ansible.posix.authorized_key: user: "{{ item.admin.key }}" key: "{{ lookup('file', item.key.public_key) }}" state: "{{ 'present' if item.key.active else 'absent' }}" when: item.admin.value.active with_community.general.dependent: - admin: admin_user_data - key: admin_ssh_keys[item.admin.key] loop_control: # Makes the output readable, so that it doesn't contain the whole subdictionaries and lists label: "{{ [item.admin.key, 'active' if item.key.active else 'inactive', item.key.public_key] }}" vars: admin_user_data: admin1: name: Alice active: true admin2: name: Bob active: true admin_ssh_keys: admin1: - private_key: keys/private_key_admin1.pem public_key: keys/private_key_admin1.pub active: true admin2: - private_key: keys/private_key_admin2.pem public_key: keys/private_key_admin2.pub active: true - private_key: keys/private_key_admin2-old.pem public_key: keys/private_key_admin2-old.pub active: false - name: Update DNS records community.aws.route53: zone: "{{ item.zone.key }}" record: "{{ item.prefix.key ~ '.' if item.prefix.key else '' }}{{ item.zone.key }}" type: "{{ item.entry.key }}" ttl: "{{ item.entry.value.ttl | default(3600) }}" value: "{{ item.entry.value.value }}" state: "{{ 'absent' if (item.entry.value.absent | default(False)) else 'present' }}" overwrite: true loop_control: # Makes the output readable, so that it doesn't contain the whole subdictionaries and lists label: |- {{ [item.zone.key, item.prefix.key, item.entry.key, item.entry.value.ttl | default(3600), item.entry.value.absent | default(False), item.entry.value.value] }} with_community.general.dependent: - zone: dns_setup - prefix: item.zone.value - entry: item.prefix.value vars: dns_setup: example.com: '': A: value: - 1.2.3.4 AAAA: value: - "2a01:1:2:3::1" 'test._domainkey': TXT: ttl: 300 value: - '"k=rsa; t=s; p=MIGfMA..."' example.org: 'www': A: value: - 1.2.3.4 - 5.6.7.8 """ RETURN = """ _list: description: - A list composed of dictionaries whose keys are the variable names from the input list. type: list elements: dict sample: - key1: a key2: test - key1: a key2: foo - key1: b key2: bar """ from ansible.errors import AnsibleLookupError from ansible.module_utils.common._collections_compat import Mapping, Sequence from ansible.module_utils.six import string_types from ansible.plugins.lookup import LookupBase from ansible.release import __version__ as ansible_version from ansible.template import Templar from ansible_collections.community.general.plugins.module_utils.version import LooseVersion # Whether Templar has a cache, which can be controlled by Templar.template()'s cache option. # The cache was removed for ansible-core 2.14 (https://github.com/ansible/ansible/pull/78419) _TEMPLAR_HAS_TEMPLATE_CACHE = LooseVersion(ansible_version) < LooseVersion('2.14.0') class LookupModule(LookupBase): def __evaluate(self, expression, templar, variables): """Evaluate expression with templar. ``expression`` is the expression to evaluate. ``variables`` are the variables to use. """ templar.available_variables = variables or {} expression = "{0}{1}{2}".format("{{", expression, "}}") if _TEMPLAR_HAS_TEMPLATE_CACHE: return templar.template(expression, cache=False) return templar.template(expression) def __process(self, result, terms, index, current, templar, variables): """Fills ``result`` list with evaluated items. ``result`` is a list where the resulting items are placed. ``terms`` is the parsed list of terms ``index`` is the current index to be processed in the list. ``current`` is a dictionary where the first ``index`` values are filled in. ``variables`` are the variables currently available. """ # If we are done, add to result list: if index == len(terms): result.append(current.copy()) return key, expression, values = terms[index] if expression is not None: # Evaluate expression in current context vars = variables.copy() vars['item'] = current.copy() try: values = self.__evaluate(expression, templar, variables=vars) except Exception as e: raise AnsibleLookupError( 'Caught "{error}" while evaluating {key!r} with item == {item!r}'.format( error=e, key=key, item=current)) if isinstance(values, Mapping): for idx, val in sorted(values.items()): current[key] = dict([('key', idx), ('value', val)]) self.__process(result, terms, index + 1, current, templar, variables) elif isinstance(values, Sequence): for elt in values: current[key] = elt self.__process(result, terms, index + 1, current, templar, variables) else: raise AnsibleLookupError( 'Did not obtain dictionary or list while evaluating {key!r} with item == {item!r}, but {type}'.format( key=key, item=current, type=type(values))) def run(self, terms, variables=None, **kwargs): """Generate list.""" result = [] if len(terms) > 0: templar = Templar(loader=self._templar._loader) data = [] vars_so_far = set() for index, term in enumerate(terms): if not isinstance(term, Mapping): raise AnsibleLookupError( 'Parameter {index} must be a dictionary, got {type}'.format( index=index, type=type(term))) if len(term) != 1: raise AnsibleLookupError( 'Parameter {index} must be a one-element dictionary, got {count} elements'.format( index=index, count=len(term))) k, v = list(term.items())[0] if k in vars_so_far: raise AnsibleLookupError( 'The variable {key!r} appears more than once'.format(key=k)) vars_so_far.add(k) if isinstance(v, string_types): data.append((k, v, None)) elif isinstance(v, (Sequence, Mapping)): data.append((k, None, v)) else: raise AnsibleLookupError( 'Parameter {key!r} (index {index}) must have a value of type string, dictionary or list, got type {type}'.format( index=index, key=k, type=type(v))) self.__process(result, data, 0, {}, templar, variables) return result