summaryrefslogtreecommitdiff
path: root/lib/ansible/template/native_helpers.py
blob: e306ec320159af0f29c23a559c5715c74abe9f9d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# Copyright: (c) 2018, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type


import ast
from itertools import islice, chain
from types import GeneratorType

from jinja2.runtime import StrictUndefined

from ansible.module_utils._text import to_text
from ansible.module_utils.common.collections import is_sequence, Mapping
from ansible.module_utils.six import string_types
from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
from ansible.utils.native_jinja import NativeJinjaText
from ansible.utils.unsafe_proxy import wrap_var


_JSON_MAP = {
    "true": True,
    "false": False,
    "null": None,
}


class Json2Python(ast.NodeTransformer):
    def visit_Name(self, node):
        if node.id not in _JSON_MAP:
            return node
        return ast.Constant(value=_JSON_MAP[node.id])


def _fail_on_undefined(data):
    """Recursively find an undefined value in a nested data structure
    and properly raise the undefined exception.
    """
    if isinstance(data, Mapping):
        for value in data.values():
            _fail_on_undefined(value)
    elif is_sequence(data):
        for item in data:
            _fail_on_undefined(item)
    else:
        if isinstance(data, StrictUndefined):
            # To actually raise the undefined exception we need to
            # access the undefined object otherwise the exception would
            # be raised on the next access which might not be properly
            # handled.
            # See https://github.com/ansible/ansible/issues/52158
            # and StrictUndefined implementation in upstream Jinja2.
            str(data)

    return data


def ansible_eval_concat(nodes):
    """Return a string of concatenated compiled nodes. Throw an undefined error
    if any of the nodes is undefined.

    If the result of concat appears to be a dictionary, list or bool,
    try and convert it to such using literal_eval, the same mechanism as used
    in jinja2_native.

    Used in Templar.template() when jinja2_native=False and convert_data=True.
    """
    head = list(islice(nodes, 2))

    if not head:
        return ''

    if len(head) == 1:
        out = _fail_on_undefined(head[0])

        if isinstance(out, NativeJinjaText):
            return out

        out = to_text(out)
    else:
        if isinstance(nodes, GeneratorType):
            nodes = chain(head, nodes)
        out = ''.join([to_text(_fail_on_undefined(v)) for v in nodes])

    # if this looks like a dictionary, list or bool, convert it to such
    if out.startswith(('{', '[')) or out in ('True', 'False'):
        unsafe = hasattr(out, '__UNSAFE__')
        try:
            out = ast.literal_eval(
                ast.fix_missing_locations(
                    Json2Python().visit(
                        ast.parse(out, mode='eval')
                    )
                )
            )
        except (ValueError, SyntaxError, MemoryError):
            pass
        else:
            if unsafe:
                out = wrap_var(out)

    return out


def ansible_concat(nodes):
    """Return a string of concatenated compiled nodes. Throw an undefined error
    if any of the nodes is undefined. Other than that it is equivalent to
    Jinja2's default concat function.

    Used in Templar.template() when jinja2_native=False and convert_data=False.
    """
    return ''.join([to_text(_fail_on_undefined(v)) for v in nodes])


def ansible_native_concat(nodes):
    """Return a native Python type from the list of compiled nodes. If the
    result is a single node, its value is returned. Otherwise, the nodes are
    concatenated as strings. If the result can be parsed with
    :func:`ast.literal_eval`, the parsed value is returned. Otherwise, the
    string is returned.

    https://github.com/pallets/jinja/blob/master/src/jinja2/nativetypes.py
    """
    head = list(islice(nodes, 2))

    if not head:
        return None

    if len(head) == 1:
        out = _fail_on_undefined(head[0])

        # TODO send unvaulted data to literal_eval?
        if isinstance(out, AnsibleVaultEncryptedUnicode):
            return out.data

        if isinstance(out, NativeJinjaText):
            # Sometimes (e.g. ``| string``) we need to mark variables
            # in a special way so that they remain strings and are not
            # passed into literal_eval.
            # See:
            # https://github.com/ansible/ansible/issues/70831
            # https://github.com/pallets/jinja/issues/1200
            # https://github.com/ansible/ansible/issues/70831#issuecomment-664190894
            return out

        # short-circuit literal_eval for anything other than strings
        if not isinstance(out, string_types):
            return out
    else:
        if isinstance(nodes, GeneratorType):
            nodes = chain(head, nodes)
        out = ''.join([to_text(_fail_on_undefined(v)) for v in nodes])

    try:
        return ast.literal_eval(
            # In Python 3.10+ ast.literal_eval removes leading spaces/tabs
            # from the given string. For backwards compatibility we need to
            # parse the string ourselves without removing leading spaces/tabs.
            ast.parse(out, mode='eval')
        )
    except (ValueError, SyntaxError, MemoryError):
        return out