2014-11-14 23:14:08 +01:00
|
|
|
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
|
|
|
#
|
|
|
|
# This file is part of Ansible
|
|
|
|
#
|
|
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
# Make coding more python3-ish
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
|
2015-07-03 22:27:49 +02:00
|
|
|
import ast
|
2015-09-25 20:54:20 +02:00
|
|
|
import contextlib
|
|
|
|
import os
|
2015-04-13 18:17:07 +02:00
|
|
|
import re
|
2016-03-02 15:34:07 +01:00
|
|
|
|
2016-03-02 16:47:10 +01:00
|
|
|
from io import StringIO
|
2016-09-07 07:54:17 +02:00
|
|
|
from numbers import Number
|
2015-04-13 18:17:07 +02:00
|
|
|
|
2017-03-23 21:35:05 +01:00
|
|
|
try:
|
|
|
|
from hashlib import sha1
|
|
|
|
except ImportError:
|
|
|
|
from sha import sha as sha1
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
from jinja2 import Environment
|
2015-06-18 22:10:01 +02:00
|
|
|
from jinja2.loaders import FileSystemLoader
|
2014-11-14 23:14:08 +01:00
|
|
|
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
2017-01-20 06:13:09 +01:00
|
|
|
from jinja2.utils import concat as j2_concat, missing
|
2016-12-13 18:14:47 +01:00
|
|
|
from jinja2.runtime import Context, StrictUndefined
|
2017-03-23 21:35:05 +01:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
from ansible import constants as C
|
2015-01-12 23:04:56 +01:00
|
|
|
from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleUndefinedVariable
|
2017-03-23 21:35:05 +01:00
|
|
|
from ansible.module_utils.six import string_types, text_type
|
|
|
|
from ansible.module_utils._text import to_native, to_text
|
2015-09-10 19:39:32 +02:00
|
|
|
from ansible.plugins import filter_loader, lookup_loader, test_loader
|
2014-11-14 23:14:08 +01:00
|
|
|
from ansible.template.safe_eval import safe_eval
|
|
|
|
from ansible.template.template import AnsibleJ2Template
|
|
|
|
from ansible.template.vars import AnsibleJ2Vars
|
2015-03-14 21:26:48 +01:00
|
|
|
|
2016-02-02 00:59:14 +01:00
|
|
|
try:
|
|
|
|
from __main__ import display
|
|
|
|
except ImportError:
|
|
|
|
from ansible.utils.display import Display
|
|
|
|
display = Display()
|
|
|
|
|
2017-03-23 21:35:05 +01:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
__all__ = ['Templar']
|
|
|
|
|
2015-03-14 21:26:48 +01:00
|
|
|
# A regex for checking to see if a variable we're trying to
|
|
|
|
# expand is just a single variable name.
|
|
|
|
|
|
|
|
# Primitive Types which we don't want Jinja to convert to strings.
|
|
|
|
NON_TEMPLATED_TYPES = ( bool, Number )
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
JINJA2_OVERRIDE = '#jinja2:'
|
|
|
|
|
2015-09-25 20:54:20 +02:00
|
|
|
|
2015-09-04 02:45:17 +02:00
|
|
|
def _escape_backslashes(data, jinja_env):
|
2015-08-31 21:47:36 +02:00
|
|
|
"""Double backslashes within jinja2 expressions
|
|
|
|
|
|
|
|
A user may enter something like this in a playbook::
|
|
|
|
|
|
|
|
debug:
|
|
|
|
msg: "Test Case 1\\3; {{ test1_name | regex_replace('^(.*)_name$', '\\1')}}"
|
|
|
|
|
|
|
|
The string inside of the {{ gets interpreted multiple times First by yaml.
|
|
|
|
Then by python. And finally by jinja2 as part of it's variable. Because
|
|
|
|
it is processed by both python and jinja2, the backslash escaped
|
|
|
|
characters get unescaped twice. This means that we'd normally have to use
|
|
|
|
four backslashes to escape that. This is painful for playbook authors as
|
|
|
|
they have to remember different rules for inside vs outside of a jinja2
|
|
|
|
expression (The backslashes outside of the "{{ }}" only get processed by
|
|
|
|
yaml and python. So they only need to be escaped once). The following
|
|
|
|
code fixes this by automatically performing the extra quoting of
|
|
|
|
backslashes inside of a jinja2 expression.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if '\\' in data and '{{' in data:
|
|
|
|
new_data = []
|
|
|
|
d2 = jinja_env.preprocess(data)
|
|
|
|
in_var = False
|
|
|
|
|
|
|
|
for token in jinja_env.lex(d2):
|
|
|
|
if token[1] == 'variable_begin':
|
|
|
|
in_var = True
|
|
|
|
new_data.append(token[2])
|
|
|
|
elif token[1] == 'variable_end':
|
|
|
|
in_var = False
|
|
|
|
new_data.append(token[2])
|
|
|
|
elif in_var and token[1] == 'string':
|
|
|
|
# Double backslashes only if we're inside of a jinja2 variable
|
|
|
|
new_data.append(token[2].replace('\\','\\\\'))
|
|
|
|
else:
|
|
|
|
new_data.append(token[2])
|
|
|
|
|
|
|
|
data = ''.join(new_data)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
2016-09-07 07:54:17 +02:00
|
|
|
|
2015-09-02 17:52:03 +02:00
|
|
|
def _count_newlines_from_end(in_str):
|
|
|
|
'''
|
|
|
|
Counts the number of newlines at the end of a string. This is used during
|
|
|
|
the jinja2 templating to ensure the count matches the input, since some newlines
|
|
|
|
may be thrown away during the templating.
|
|
|
|
'''
|
|
|
|
|
2015-09-02 19:57:40 +02:00
|
|
|
try:
|
|
|
|
i = len(in_str)
|
|
|
|
j = i -1
|
|
|
|
while in_str[j] == '\n':
|
|
|
|
j -= 1
|
|
|
|
return i - 1 - j
|
|
|
|
except IndexError:
|
|
|
|
# Uncommon cases: zero length string and string containing only newlines
|
|
|
|
return i
|
2015-09-02 17:52:03 +02:00
|
|
|
|
2016-12-13 18:14:47 +01:00
|
|
|
class AnsibleContext(Context):
|
|
|
|
'''
|
|
|
|
A custom context, which intercepts resolve() calls and sets a flag
|
|
|
|
internally if any variable lookup returns an AnsibleUnsafe value. This
|
|
|
|
flag is checked post-templating, and (when set) will result in the
|
|
|
|
final templated result being wrapped via UnsafeProxy.
|
|
|
|
'''
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(AnsibleContext, self).__init__(*args, **kwargs)
|
|
|
|
self.unsafe = False
|
|
|
|
|
|
|
|
def _is_unsafe(self, val):
|
|
|
|
'''
|
|
|
|
Our helper function, which will also recursively check dict and
|
|
|
|
list entries due to the fact that they may be repr'd and contain
|
|
|
|
a key or value which contains jinja2 syntax and would otherwise
|
|
|
|
lose the AnsibleUnsafe value.
|
|
|
|
'''
|
|
|
|
if isinstance(val, dict):
|
|
|
|
for key in val.keys():
|
2017-02-08 18:36:54 +01:00
|
|
|
if self._is_unsafe(val[key]):
|
2016-12-13 18:14:47 +01:00
|
|
|
return True
|
|
|
|
elif isinstance(val, list):
|
|
|
|
for item in val:
|
|
|
|
if self._is_unsafe(item):
|
|
|
|
return True
|
|
|
|
elif isinstance(val, string_types) and hasattr(val, '__UNSAFE__'):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2017-01-20 06:13:09 +01:00
|
|
|
def _update_unsafe(self, val):
|
|
|
|
if val is not None and not self.unsafe and self._is_unsafe(val):
|
|
|
|
self.unsafe = True
|
|
|
|
|
2016-12-13 18:14:47 +01:00
|
|
|
def resolve(self, key):
|
|
|
|
'''
|
|
|
|
The intercepted resolve(), which uses the helper above to set the
|
|
|
|
internal flag whenever an unsafe variable value is returned.
|
|
|
|
'''
|
|
|
|
val = super(AnsibleContext, self).resolve(key)
|
2017-01-20 06:13:09 +01:00
|
|
|
self._update_unsafe(val)
|
|
|
|
return val
|
|
|
|
|
|
|
|
def resolve_or_missing(self, key):
|
|
|
|
val = super(AnsibleContext, self).resolve_or_missing(key)
|
|
|
|
self._update_unsafe(val)
|
2016-12-13 18:14:47 +01:00
|
|
|
return val
|
|
|
|
|
|
|
|
class AnsibleEnvironment(Environment):
|
|
|
|
'''
|
|
|
|
Our custom environment, which simply allows us to override the class-level
|
|
|
|
values for the Template and Context classes used by jinja2 internally.
|
|
|
|
'''
|
|
|
|
context_class = AnsibleContext
|
|
|
|
template_class = AnsibleJ2Template
|
2015-09-25 20:54:20 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
class Templar:
|
|
|
|
'''
|
|
|
|
The main class for templating, with the main entry-point of template().
|
|
|
|
'''
|
|
|
|
|
2015-06-11 06:21:53 +02:00
|
|
|
def __init__(self, loader, shared_loader_obj=None, variables=dict()):
|
2015-01-09 16:37:31 +01:00
|
|
|
self._loader = loader
|
2014-11-14 23:14:08 +01:00
|
|
|
self._filters = None
|
2014-11-27 01:58:45 +01:00
|
|
|
self._tests = None
|
2014-11-14 23:14:08 +01:00
|
|
|
self._available_variables = variables
|
2015-11-01 22:30:55 +01:00
|
|
|
self._cached_result = {}
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2015-07-24 16:31:14 +02:00
|
|
|
if loader:
|
|
|
|
self._basedir = loader.get_basedir()
|
|
|
|
else:
|
|
|
|
self._basedir = './'
|
|
|
|
|
2015-05-02 06:48:11 +02:00
|
|
|
if shared_loader_obj:
|
|
|
|
self._filter_loader = getattr(shared_loader_obj, 'filter_loader')
|
2015-10-14 23:50:23 +02:00
|
|
|
self._test_loader = getattr(shared_loader_obj, 'test_loader')
|
2015-05-02 06:48:11 +02:00
|
|
|
self._lookup_loader = getattr(shared_loader_obj, 'lookup_loader')
|
|
|
|
else:
|
|
|
|
self._filter_loader = filter_loader
|
2015-10-14 23:50:23 +02:00
|
|
|
self._test_loader = test_loader
|
2015-05-02 06:48:11 +02:00
|
|
|
self._lookup_loader = lookup_loader
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
# flags to determine whether certain failures during templating
|
|
|
|
# should result in fatal errors being raised
|
|
|
|
self._fail_on_lookup_errors = True
|
|
|
|
self._fail_on_filter_errors = True
|
2015-06-11 06:21:53 +02:00
|
|
|
self._fail_on_undefined_errors = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
|
|
|
|
|
2016-12-13 18:14:47 +01:00
|
|
|
self.environment = AnsibleEnvironment(
|
2015-06-18 22:10:01 +02:00
|
|
|
trim_blocks=True,
|
|
|
|
undefined=StrictUndefined,
|
|
|
|
extensions=self._get_extensions(),
|
|
|
|
finalize=self._finalize,
|
2015-08-05 04:49:42 +02:00
|
|
|
loader=FileSystemLoader(self._basedir),
|
2015-06-18 22:10:01 +02:00
|
|
|
)
|
2015-06-11 06:21:53 +02:00
|
|
|
|
|
|
|
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2015-09-25 20:54:20 +02:00
|
|
|
self.block_start = self.environment.block_start_string
|
|
|
|
self.block_end = self.environment.block_end_string
|
|
|
|
self.variable_start = self.environment.variable_start_string
|
|
|
|
self.variable_end = self.environment.variable_end_string
|
2015-11-13 03:42:39 +01:00
|
|
|
self._clean_regex = re.compile(r'(?:%s|%s|%s|%s)' % (self.variable_start, self.block_start, self.block_end, self.variable_end))
|
2016-06-15 20:17:17 +02:00
|
|
|
self._no_type_regex = re.compile(r'.*\|\s*(?:%s)\s*(?:%s)?$' % ('|'.join(C.STRING_TYPE_FILTERS), self.variable_end))
|
2015-09-25 20:54:20 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
def _get_filters(self):
|
|
|
|
'''
|
|
|
|
Returns filter plugins, after loading and caching them if need be
|
|
|
|
'''
|
|
|
|
|
|
|
|
if self._filters is not None:
|
|
|
|
return self._filters.copy()
|
|
|
|
|
2015-05-02 06:48:11 +02:00
|
|
|
plugins = [x for x in self._filter_loader.all()]
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
self._filters = dict()
|
|
|
|
for fp in plugins:
|
|
|
|
self._filters.update(fp.filters())
|
2014-11-27 01:58:45 +01:00
|
|
|
self._filters.update(self._get_tests())
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
return self._filters.copy()
|
|
|
|
|
2014-11-27 01:58:45 +01:00
|
|
|
def _get_tests(self):
|
|
|
|
'''
|
|
|
|
Returns tests plugins, after loading and caching them if need be
|
|
|
|
'''
|
|
|
|
|
|
|
|
if self._tests is not None:
|
|
|
|
return self._tests.copy()
|
|
|
|
|
2015-10-14 23:50:23 +02:00
|
|
|
plugins = [x for x in self._test_loader.all()]
|
2014-11-27 01:58:45 +01:00
|
|
|
|
|
|
|
self._tests = dict()
|
|
|
|
for fp in plugins:
|
|
|
|
self._tests.update(fp.tests())
|
|
|
|
|
|
|
|
return self._tests.copy()
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
def _get_extensions(self):
|
2014-11-27 01:58:45 +01:00
|
|
|
'''
|
2014-11-14 23:14:08 +01:00
|
|
|
Return jinja2 extensions to load.
|
|
|
|
|
|
|
|
If some extensions are set via jinja_extensions in ansible.cfg, we try
|
|
|
|
to load them with the jinja environment.
|
|
|
|
'''
|
|
|
|
|
|
|
|
jinja_exts = []
|
|
|
|
if C.DEFAULT_JINJA2_EXTENSIONS:
|
|
|
|
# make sure the configuration directive doesn't contain spaces
|
|
|
|
# and split extensions in an array
|
|
|
|
jinja_exts = C.DEFAULT_JINJA2_EXTENSIONS.replace(" ", "").split(',')
|
|
|
|
|
|
|
|
return jinja_exts
|
|
|
|
|
2015-09-25 20:54:20 +02:00
|
|
|
def _clean_data(self, orig_data):
|
|
|
|
''' remove jinja2 template tags from a string '''
|
|
|
|
|
2017-02-10 16:34:35 +01:00
|
|
|
if not isinstance(orig_data, string_types) or hasattr(orig_data, '__ENCRYPTED__'):
|
2015-09-25 20:54:20 +02:00
|
|
|
return orig_data
|
|
|
|
|
2015-09-28 08:26:36 +02:00
|
|
|
with contextlib.closing(StringIO(orig_data)) as data:
|
2015-09-25 20:54:20 +02:00
|
|
|
# these variables keep track of opening block locations, as we only
|
|
|
|
# want to replace matched pairs of print/block tags
|
|
|
|
print_openings = []
|
|
|
|
block_openings = []
|
|
|
|
for mo in self._clean_regex.finditer(orig_data):
|
|
|
|
token = mo.group(0)
|
|
|
|
token_start = mo.start(0)
|
|
|
|
|
|
|
|
if token[0] == self.variable_start[0]:
|
|
|
|
if token == self.block_start:
|
|
|
|
block_openings.append(token_start)
|
|
|
|
elif token == self.variable_start:
|
|
|
|
print_openings.append(token_start)
|
|
|
|
|
|
|
|
elif token[1] == self.variable_end[1]:
|
|
|
|
prev_idx = None
|
2015-11-02 15:00:56 +01:00
|
|
|
if token == self.block_end and block_openings:
|
2015-09-25 20:54:20 +02:00
|
|
|
prev_idx = block_openings.pop()
|
2015-11-02 15:00:56 +01:00
|
|
|
elif token == self.variable_end and print_openings:
|
2015-09-25 20:54:20 +02:00
|
|
|
prev_idx = print_openings.pop()
|
|
|
|
|
|
|
|
if prev_idx is not None:
|
|
|
|
# replace the opening
|
|
|
|
data.seek(prev_idx, os.SEEK_SET)
|
2016-09-07 07:54:17 +02:00
|
|
|
data.write(to_text(self.environment.comment_start_string))
|
2015-09-25 20:54:20 +02:00
|
|
|
# replace the closing
|
|
|
|
data.seek(token_start, os.SEEK_SET)
|
2016-09-07 07:54:17 +02:00
|
|
|
data.write(to_text(self.environment.comment_end_string))
|
2015-09-25 20:54:20 +02:00
|
|
|
|
|
|
|
else:
|
|
|
|
raise AnsibleError("Error while cleaning data for safety: unhandled regex match")
|
|
|
|
|
|
|
|
return data.getvalue()
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
def set_available_variables(self, variables):
|
|
|
|
'''
|
|
|
|
Sets the list of template variables this Templar instance will use
|
|
|
|
to template things, so we don't have to pass them around between
|
2015-11-07 04:04:52 +01:00
|
|
|
internal methods. We also clear the template cache here, as the variables
|
|
|
|
are being changed.
|
2014-11-14 23:14:08 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
assert isinstance(variables, dict)
|
2015-11-04 17:26:06 +01:00
|
|
|
self._available_variables = variables
|
2015-11-02 07:57:44 +01:00
|
|
|
self._cached_result = {}
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2017-03-23 02:50:28 +01:00
|
|
|
def template(self, variable, convert_bare=False, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None,
|
|
|
|
convert_data=True, static_vars=[''], cache=True, bare_deprecated=True, disable_lookups=False):
|
2014-11-14 23:14:08 +01:00
|
|
|
'''
|
|
|
|
Templates (possibly recursively) any given data as input. If convert_bare is
|
|
|
|
set to True, the given data will be wrapped as a jinja2 variable ('{{foo}}')
|
2016-08-24 02:03:11 +02:00
|
|
|
before being sent through the template engine.
|
2014-11-14 23:14:08 +01:00
|
|
|
'''
|
|
|
|
|
2015-11-02 21:04:20 +01:00
|
|
|
if fail_on_undefined is None:
|
|
|
|
fail_on_undefined = self._fail_on_undefined_errors
|
|
|
|
|
2015-11-13 03:42:39 +01:00
|
|
|
# Don't template unsafe variables, instead drop them back down to their constituent type.
|
2015-09-04 00:08:34 +02:00
|
|
|
if hasattr(variable, '__UNSAFE__'):
|
2015-09-24 11:52:51 +02:00
|
|
|
if isinstance(variable, text_type):
|
2016-12-13 18:14:47 +01:00
|
|
|
rval = self._clean_data(variable)
|
2017-02-08 22:09:34 +01:00
|
|
|
return rval
|
2015-09-04 00:08:34 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
try:
|
|
|
|
if convert_bare:
|
2016-02-02 00:59:14 +01:00
|
|
|
variable = self._convert_bare_variable(variable, bare_deprecated=bare_deprecated)
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2015-09-09 08:25:23 +02:00
|
|
|
if isinstance(variable, string_types):
|
2014-11-14 23:14:08 +01:00
|
|
|
result = variable
|
2015-11-13 03:42:39 +01:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
if self._contains_vars(variable):
|
2015-03-14 21:26:48 +01:00
|
|
|
# Check to see if the string we are trying to render is just referencing a single
|
2015-04-28 15:36:42 +02:00
|
|
|
# var. In this case we don't want to accidentally change the type of the variable
|
2015-03-14 21:26:48 +01:00
|
|
|
# to a string by using the jinja template renderer. We just want to pass it.
|
2015-06-11 06:21:53 +02:00
|
|
|
only_one = self.SINGLE_VAR.match(variable)
|
2015-03-14 21:26:48 +01:00
|
|
|
if only_one:
|
|
|
|
var_name = only_one.group(1)
|
2015-04-13 21:57:17 +02:00
|
|
|
if var_name in self._available_variables:
|
|
|
|
resolved_val = self._available_variables[var_name]
|
2015-03-14 21:26:48 +01:00
|
|
|
if isinstance(resolved_val, NON_TEMPLATED_TYPES):
|
|
|
|
return resolved_val
|
2015-08-27 08:25:51 +02:00
|
|
|
elif resolved_val is None:
|
2015-05-08 16:12:36 +02:00
|
|
|
return C.DEFAULT_NULL_REPRESENTATION
|
2015-03-14 21:26:48 +01:00
|
|
|
|
2015-11-01 22:30:55 +01:00
|
|
|
# Using a cache in order to prevent template calls with already templated variables
|
2015-11-09 22:30:32 +01:00
|
|
|
sha1_hash = None
|
|
|
|
if cache:
|
|
|
|
variable_hash = sha1(text_type(variable).encode('utf-8'))
|
2017-03-23 02:50:28 +01:00
|
|
|
options_hash = sha1(
|
|
|
|
(
|
|
|
|
text_type(preserve_trailing_newlines) +
|
|
|
|
text_type(escape_backslashes) +
|
|
|
|
text_type(fail_on_undefined) +
|
|
|
|
text_type(overrides)
|
|
|
|
).encode('utf-8')
|
|
|
|
)
|
2015-11-09 22:30:32 +01:00
|
|
|
sha1_hash = variable_hash.hexdigest() + options_hash.hexdigest()
|
|
|
|
if cache and sha1_hash in self._cached_result:
|
2015-11-02 21:04:20 +01:00
|
|
|
result = self._cached_result[sha1_hash]
|
|
|
|
else:
|
2016-12-13 18:14:47 +01:00
|
|
|
result = self.do_template(
|
|
|
|
variable,
|
|
|
|
preserve_trailing_newlines=preserve_trailing_newlines,
|
|
|
|
escape_backslashes=escape_backslashes,
|
|
|
|
fail_on_undefined=fail_on_undefined,
|
|
|
|
overrides=overrides,
|
2017-01-10 23:54:00 +01:00
|
|
|
disable_lookups=disable_lookups,
|
2016-12-13 18:14:47 +01:00
|
|
|
)
|
2017-02-10 16:34:35 +01:00
|
|
|
|
|
|
|
unsafe = hasattr(result, '__UNSAFE__')
|
2017-01-11 17:45:46 +01:00
|
|
|
if convert_data and not self._no_type_regex.match(variable):
|
2015-11-01 22:30:55 +01:00
|
|
|
# if this looks like a dictionary or list, convert it to such using the safe_eval method
|
|
|
|
if (result.startswith("{") and not result.startswith(self.environment.variable_start_string)) or \
|
2016-09-07 07:54:17 +02:00
|
|
|
result.startswith("[") or result in ("True", "False"):
|
2015-11-01 22:30:55 +01:00
|
|
|
eval_results = safe_eval(result, locals=self._available_variables, include_exceptions=True)
|
|
|
|
if eval_results[1] is None:
|
|
|
|
result = eval_results[0]
|
2016-12-13 18:14:47 +01:00
|
|
|
if unsafe:
|
2017-01-10 23:54:00 +01:00
|
|
|
from ansible.vars.unsafe_proxy import wrap_var
|
2016-12-13 18:14:47 +01:00
|
|
|
result = wrap_var(result)
|
2015-11-01 22:30:55 +01:00
|
|
|
else:
|
|
|
|
# FIXME: if the safe_eval raised an error, should we do something with it?
|
|
|
|
pass
|
2015-02-03 04:08:10 +01:00
|
|
|
|
2015-11-09 20:07:03 +01:00
|
|
|
# we only cache in the case where we have a single variable
|
|
|
|
# name, to make sure we're not putting things which may otherwise
|
|
|
|
# be dynamic in the cache (filters, lookups, etc.)
|
2015-11-09 22:30:32 +01:00
|
|
|
if cache:
|
2015-11-09 20:07:03 +01:00
|
|
|
self._cached_result[sha1_hash] = result
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
elif isinstance(variable, (list, tuple)):
|
2017-01-12 16:40:19 +01:00
|
|
|
return [self.template(
|
2017-01-29 08:28:53 +01:00
|
|
|
v,
|
|
|
|
preserve_trailing_newlines=preserve_trailing_newlines,
|
|
|
|
fail_on_undefined=fail_on_undefined,
|
|
|
|
overrides=overrides,
|
|
|
|
disable_lookups=disable_lookups,
|
|
|
|
) for v in variable]
|
2014-11-14 23:14:08 +01:00
|
|
|
elif isinstance(variable, dict):
|
|
|
|
d = {}
|
2015-09-01 20:11:23 +02:00
|
|
|
# we don't use iteritems() here to avoid problems if the underlying dict
|
|
|
|
# changes sizes due to the templating, which can happen with hostvars
|
|
|
|
for k in variable.keys():
|
2015-11-04 22:16:14 +01:00
|
|
|
if k not in static_vars:
|
2017-01-12 16:40:19 +01:00
|
|
|
d[k] = self.template(
|
2017-01-29 08:28:53 +01:00
|
|
|
variable[k],
|
|
|
|
preserve_trailing_newlines=preserve_trailing_newlines,
|
|
|
|
fail_on_undefined=fail_on_undefined,
|
|
|
|
overrides=overrides,
|
|
|
|
disable_lookups=disable_lookups,
|
|
|
|
)
|
2015-11-04 22:16:14 +01:00
|
|
|
else:
|
|
|
|
d[k] = variable[k]
|
2014-11-14 23:14:08 +01:00
|
|
|
return d
|
|
|
|
else:
|
|
|
|
return variable
|
|
|
|
|
|
|
|
except AnsibleFilterError:
|
|
|
|
if self._fail_on_filter_errors:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
return variable
|
|
|
|
|
2016-09-06 02:07:58 +02:00
|
|
|
def templatable(self, data):
|
|
|
|
'''
|
|
|
|
returns True if the data can be templated w/o errors
|
|
|
|
'''
|
|
|
|
templatable = True
|
|
|
|
try:
|
|
|
|
self.template(data)
|
|
|
|
except:
|
2017-01-31 00:01:47 +01:00
|
|
|
templatable = False
|
2016-09-06 02:07:58 +02:00
|
|
|
return templatable
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
def _contains_vars(self, data):
|
|
|
|
'''
|
|
|
|
returns True if the data contains a variable pattern
|
|
|
|
'''
|
2016-09-06 02:07:58 +02:00
|
|
|
if isinstance(data, string_types):
|
2016-09-07 07:54:17 +02:00
|
|
|
for marker in (self.environment.block_start_string, self.environment.variable_start_string, self.environment.comment_start_string):
|
2016-09-06 02:07:58 +02:00
|
|
|
if marker in data:
|
|
|
|
return True
|
2016-02-16 15:42:33 +01:00
|
|
|
return False
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2016-02-02 00:59:14 +01:00
|
|
|
def _convert_bare_variable(self, variable, bare_deprecated):
|
2014-11-14 23:14:08 +01:00
|
|
|
'''
|
|
|
|
Wraps a bare string, which may have an attribute portion (ie. foo.bar)
|
|
|
|
in jinja2 variable braces so that it is evaluated properly.
|
|
|
|
'''
|
|
|
|
|
2015-09-09 08:25:23 +02:00
|
|
|
if isinstance(variable, string_types):
|
2015-07-22 06:08:32 +02:00
|
|
|
contains_filters = "|" in variable
|
|
|
|
first_part = variable.split("|")[0].split(".")[0].split("[")[0]
|
|
|
|
if (contains_filters or first_part in self._available_variables) and self.environment.variable_start_string not in variable:
|
2016-02-02 00:59:14 +01:00
|
|
|
if bare_deprecated:
|
2016-09-07 07:54:17 +02:00
|
|
|
display.deprecated("Using bare variables is deprecated."
|
|
|
|
" Update your playbooks so that the environment value uses the full variable syntax ('%s%s%s')" %
|
|
|
|
(self.environment.variable_start_string, variable, self.environment.variable_end_string))
|
2015-06-11 06:21:53 +02:00
|
|
|
return "%s%s%s" % (self.environment.variable_start_string, variable, self.environment.variable_end_string)
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
# the variable didn't meet the conditions to be converted,
|
|
|
|
# so just return it as-is
|
|
|
|
return variable
|
|
|
|
|
|
|
|
def _finalize(self, thing):
|
|
|
|
'''
|
|
|
|
A custom finalize method for jinja2, which prevents None from being returned
|
|
|
|
'''
|
|
|
|
return thing if thing is not None else ''
|
|
|
|
|
2017-01-10 23:54:00 +01:00
|
|
|
def _fail_lookup(self, name, *args, **kwargs):
|
|
|
|
raise AnsibleError("The lookup `%s` was found, however lookups were disabled from templating" % name)
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
def _lookup(self, name, *args, **kwargs):
|
2015-08-04 18:10:23 +02:00
|
|
|
instance = self._lookup_loader.get(name.lower(), loader=self._loader, templar=self)
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
if instance is not None:
|
2015-11-30 05:45:14 +01:00
|
|
|
wantlist = kwargs.pop('wantlist', False)
|
|
|
|
|
2015-08-11 23:20:16 +02:00
|
|
|
from ansible.utils.listify import listify_lookup_plugin_terms
|
2015-08-11 22:38:42 +02:00
|
|
|
loop_terms = listify_lookup_plugin_terms(terms=args, templar=self, loader=self._loader, fail_on_undefined=True, convert_bare=False)
|
2014-11-14 23:14:08 +01:00
|
|
|
# safely catch run failures per #5059
|
|
|
|
try:
|
2015-08-12 23:41:39 +02:00
|
|
|
ran = instance.run(loop_terms, variables=self._available_variables, **kwargs)
|
2015-08-16 16:16:02 +02:00
|
|
|
except (AnsibleUndefinedVariable, UndefinedError) as e:
|
|
|
|
raise AnsibleUndefinedVariable(e)
|
2015-08-27 08:16:11 +02:00
|
|
|
except Exception as e:
|
2015-01-09 16:37:31 +01:00
|
|
|
if self._fail_on_lookup_errors:
|
2017-03-23 02:50:28 +01:00
|
|
|
raise AnsibleError("An unhandled exception occurred while running the lookup plugin '%s'. Error was a %s, "
|
|
|
|
"original message: %s" % (name, type(e), e))
|
2014-11-14 23:14:08 +01:00
|
|
|
ran = None
|
2015-09-08 18:18:10 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
if ran:
|
2017-01-10 23:54:00 +01:00
|
|
|
from ansible.vars.unsafe_proxy import UnsafeProxy, wrap_var
|
2015-11-30 05:45:14 +01:00
|
|
|
if wantlist:
|
|
|
|
ran = wrap_var(ran)
|
|
|
|
else:
|
2016-02-18 22:17:42 +01:00
|
|
|
try:
|
|
|
|
ran = UnsafeProxy(",".join(ran))
|
|
|
|
except TypeError:
|
|
|
|
if isinstance(ran, list) and len(ran) == 1:
|
|
|
|
ran = wrap_var(ran[0])
|
|
|
|
else:
|
|
|
|
ran = wrap_var(ran)
|
2015-09-08 18:18:10 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
return ran
|
|
|
|
else:
|
|
|
|
raise AnsibleError("lookup plugin (%s) not found" % name)
|
|
|
|
|
2017-01-10 23:54:00 +01:00
|
|
|
def do_template(self, data, preserve_trailing_newlines=True, escape_backslashes=True, fail_on_undefined=None, overrides=None, disable_lookups=False):
|
2015-09-02 17:45:58 +02:00
|
|
|
# For preserving the number of input newlines in the output (used
|
|
|
|
# later in this method)
|
2015-09-02 17:52:03 +02:00
|
|
|
data_newlines = _count_newlines_from_end(data)
|
2015-09-02 17:45:58 +02:00
|
|
|
|
2015-06-11 06:21:53 +02:00
|
|
|
if fail_on_undefined is None:
|
|
|
|
fail_on_undefined = self._fail_on_undefined_errors
|
2014-11-14 23:14:08 +01:00
|
|
|
|
|
|
|
try:
|
2015-06-11 06:21:53 +02:00
|
|
|
# allows template header overrides to change jinja2 options.
|
|
|
|
if overrides is None:
|
|
|
|
myenv = self.environment.overlay()
|
|
|
|
else:
|
|
|
|
myenv = self.environment.overlay(overrides)
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2015-07-03 22:27:49 +02:00
|
|
|
# Get jinja env overrides from template
|
|
|
|
if data.startswith(JINJA2_OVERRIDE):
|
|
|
|
eol = data.find('\n')
|
|
|
|
line = data[len(JINJA2_OVERRIDE):eol]
|
|
|
|
data = data[eol+1:]
|
|
|
|
for pair in line.split(','):
|
|
|
|
(key,val) = pair.split(':')
|
|
|
|
key = key.strip()
|
2015-07-30 22:52:44 +02:00
|
|
|
setattr(myenv, key, ast.literal_eval(val.strip()))
|
2015-07-03 22:27:49 +02:00
|
|
|
|
2015-06-11 06:21:53 +02:00
|
|
|
#FIXME: add tests
|
|
|
|
myenv.filters.update(self._get_filters())
|
2015-07-29 21:16:27 +02:00
|
|
|
myenv.tests.update(self._get_tests())
|
2014-11-14 23:14:08 +01:00
|
|
|
|
2015-09-04 02:45:17 +02:00
|
|
|
if escape_backslashes:
|
|
|
|
# Allow users to specify backslashes in playbooks as "\\"
|
|
|
|
# instead of as "\\\\".
|
|
|
|
data = _escape_backslashes(data, myenv)
|
2015-08-31 21:47:36 +02:00
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
try:
|
2015-06-11 06:21:53 +02:00
|
|
|
t = myenv.from_string(data)
|
2015-08-27 08:16:11 +02:00
|
|
|
except TemplateSyntaxError as e:
|
2016-09-07 07:54:17 +02:00
|
|
|
raise AnsibleError("template error while templating string: %s. String: %s" % (to_native(e), to_native(data)))
|
2015-08-27 08:16:11 +02:00
|
|
|
except Exception as e:
|
2016-09-07 07:54:17 +02:00
|
|
|
if 'recursion' in to_native(e):
|
|
|
|
raise AnsibleError("recursive loop detected in template string: %s" % to_native(data))
|
2014-11-14 23:14:08 +01:00
|
|
|
else:
|
|
|
|
return data
|
|
|
|
|
2017-01-10 23:54:00 +01:00
|
|
|
if disable_lookups:
|
|
|
|
t.globals['lookup'] = self._fail_lookup
|
|
|
|
else:
|
|
|
|
t.globals['lookup'] = self._lookup
|
|
|
|
|
2014-11-14 23:14:08 +01:00
|
|
|
t.globals['finalize'] = self._finalize
|
|
|
|
|
|
|
|
jvars = AnsibleJ2Vars(self, t.globals)
|
|
|
|
|
|
|
|
new_context = t.new_context(jvars, shared=True)
|
|
|
|
rf = t.root_render_func(new_context)
|
|
|
|
|
|
|
|
try:
|
|
|
|
res = j2_concat(rf)
|
2016-12-13 18:14:47 +01:00
|
|
|
if new_context.unsafe:
|
2017-01-10 23:54:00 +01:00
|
|
|
from ansible.vars.unsafe_proxy import wrap_var
|
2016-12-13 18:14:47 +01:00
|
|
|
res = wrap_var(res)
|
2015-08-27 08:16:11 +02:00
|
|
|
except TypeError as te:
|
2016-09-07 07:54:17 +02:00
|
|
|
if 'StrictUndefined' in to_native(te):
|
|
|
|
errmsg = "Unable to look up a name or access an attribute in template string (%s).\n" % to_native(data)
|
|
|
|
errmsg += "Make sure your variable name does not contain invalid characters like '-': %s" % to_native(te)
|
2016-03-03 19:52:09 +01:00
|
|
|
raise AnsibleUndefinedVariable(errmsg)
|
2014-11-14 23:14:08 +01:00
|
|
|
else:
|
2016-09-07 07:54:17 +02:00
|
|
|
display.debug("failing because of a type error, template data is: %s" % to_native(data))
|
|
|
|
raise AnsibleError("Unexpected templating type error occurred on (%s): %s" % (to_native(data),to_native(te)))
|
2015-01-02 14:51:15 +01:00
|
|
|
|
|
|
|
if preserve_trailing_newlines:
|
|
|
|
# The low level calls above do not preserve the newline
|
|
|
|
# characters at the end of the input data, so we use the
|
|
|
|
# calculate the difference in newlines and append them
|
|
|
|
# to the resulting output for parity
|
2015-09-02 17:45:58 +02:00
|
|
|
#
|
|
|
|
# jinja2 added a keep_trailing_newline option in 2.7 when
|
|
|
|
# creating an Environment. That would let us make this code
|
|
|
|
# better (remove a single newline if
|
|
|
|
# preserve_trailing_newlines is False). Once we can depend on
|
|
|
|
# that version being present, modify our code to set that when
|
|
|
|
# initializing self.environment and remove a single trailing
|
|
|
|
# newline here if preserve_newlines is False.
|
2015-09-02 17:52:03 +02:00
|
|
|
res_newlines = _count_newlines_from_end(res)
|
2015-01-02 14:51:15 +01:00
|
|
|
if data_newlines > res_newlines:
|
|
|
|
res += '\n' * (data_newlines - res_newlines)
|
2014-11-14 23:14:08 +01:00
|
|
|
return res
|
2015-08-27 08:16:11 +02:00
|
|
|
except (UndefinedError, AnsibleUndefinedVariable) as e:
|
2015-06-11 06:21:53 +02:00
|
|
|
if fail_on_undefined:
|
2015-08-16 16:16:02 +02:00
|
|
|
raise AnsibleUndefinedVariable(e)
|
2014-11-14 23:14:08 +01:00
|
|
|
else:
|
2015-06-11 06:21:53 +02:00
|
|
|
#TODO: return warning about undefined var
|
2014-11-14 23:14:08 +01:00
|
|
|
return data
|
2016-11-15 21:16:46 +01:00
|
|
|
|
|
|
|
# for backwards compatibility in case anyone is using old private method directly
|
|
|
|
_do_template = do_template
|