summaryrefslogtreecommitdiff
path: root/v1/ansible/utils/template.py
diff options
context:
space:
mode:
Diffstat (limited to 'v1/ansible/utils/template.py')
-rw-r--r--v1/ansible/utils/template.py404
1 files changed, 404 insertions, 0 deletions
diff --git a/v1/ansible/utils/template.py b/v1/ansible/utils/template.py
new file mode 100644
index 0000000000..fb35924ce1
--- /dev/null
+++ b/v1/ansible/utils/template.py
@@ -0,0 +1,404 @@
+# (c) 2012, 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/>.
+
+import os
+import re
+import codecs
+import jinja2
+from jinja2.runtime import StrictUndefined
+from jinja2.exceptions import TemplateSyntaxError
+import yaml
+import json
+from ansible import errors
+import ansible.constants as C
+import time
+import subprocess
+import datetime
+import pwd
+import ast
+import traceback
+from numbers import Number
+
+from ansible.utils.string_functions import count_newlines_from_end
+from ansible.utils import to_bytes, to_unicode
+
+class Globals(object):
+
+ FILTERS = None
+
+ def __init__(self):
+ pass
+
+def _get_filters():
+ ''' return filter plugin instances '''
+
+ if Globals.FILTERS is not None:
+ return Globals.FILTERS
+
+ from ansible import utils
+ plugins = [ x for x in utils.plugins.filter_loader.all()]
+ filters = {}
+ for fp in plugins:
+ filters.update(fp.filters())
+ Globals.FILTERS = filters
+
+ return Globals.FILTERS
+
+def _get_extensions():
+ ''' 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:
+ '''
+ Let's 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
+
+class Flags:
+ LEGACY_TEMPLATE_WARNING = False
+
+# TODO: refactor this file
+
+FILTER_PLUGINS = None
+_LISTRE = re.compile(r"(\w+)\[(\d+)\]")
+
+# A regex for checking to see if a variable we're trying to
+# expand is just a single variable name.
+SINGLE_VAR = re.compile(r"^{{\s*(\w*)\s*}}$")
+
+JINJA2_OVERRIDE = '#jinja2:'
+JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline']
+
+def lookup(name, *args, **kwargs):
+ from ansible import utils
+ instance = utils.plugins.lookup_loader.get(name.lower(), basedir=kwargs.get('basedir',None))
+ tvars = kwargs.get('vars', None)
+
+ wantlist = kwargs.pop('wantlist', False)
+
+ if instance is not None:
+ try:
+ ran = instance.run(*args, inject=tvars, **kwargs)
+ except errors.AnsibleError:
+ raise
+ except jinja2.exceptions.UndefinedError, e:
+ raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e))
+ except Exception, e:
+ raise errors.AnsibleError('Unexpected error in during lookup: %s' % e)
+ if ran and not wantlist:
+ ran = ",".join(ran)
+ return ran
+ else:
+ raise errors.AnsibleError("lookup plugin (%s) not found" % name)
+
+def template(basedir, varname, templatevars, lookup_fatal=True, depth=0, expand_lists=True, convert_bare=False, fail_on_undefined=False, filter_fatal=True):
+ ''' templates a data structure by traversing it and substituting for other data structures '''
+ from ansible import utils
+ try:
+ if convert_bare and isinstance(varname, basestring):
+ first_part = varname.split(".")[0].split("[")[0]
+ if first_part in templatevars and '{{' not in varname and '$' not in varname:
+ varname = "{{%s}}" % varname
+
+ if isinstance(varname, basestring):
+ if '{{' in varname or '{%' in varname:
+ try:
+ varname = template_from_string(basedir, varname, templatevars, fail_on_undefined)
+ except errors.AnsibleError, e:
+ raise errors.AnsibleError("Failed to template %s: %s" % (varname, str(e)))
+
+ # template_from_string may return non strings for the case where the var is just
+ # a reference to a single variable, so we should re_check before we do further evals
+ if isinstance(varname, basestring):
+ if (varname.startswith("{") and not varname.startswith("{{")) or varname.startswith("["):
+ eval_results = utils.safe_eval(varname, locals=templatevars, include_exceptions=True)
+ if eval_results[1] is None:
+ varname = eval_results[0]
+
+ return varname
+
+ elif isinstance(varname, (list, tuple)):
+ return [template(basedir, v, templatevars, lookup_fatal, depth, expand_lists, convert_bare, fail_on_undefined, filter_fatal) for v in varname]
+ elif isinstance(varname, dict):
+ d = {}
+ for (k, v) in varname.iteritems():
+ d[k] = template(basedir, v, templatevars, lookup_fatal, depth, expand_lists, convert_bare, fail_on_undefined, filter_fatal)
+ return d
+ else:
+ return varname
+ except errors.AnsibleFilterError:
+ if filter_fatal:
+ raise
+ else:
+ return varname
+
+
+class _jinja2_vars(object):
+ '''
+ Helper class to template all variable content before jinja2 sees it.
+ This is done by hijacking the variable storage that jinja2 uses, and
+ overriding __contains__ and __getitem__ to look like a dict. Added bonus
+ is avoiding duplicating the large hashes that inject tends to be.
+ To facilitate using builtin jinja2 things like range, globals are handled
+ here.
+ extras is a list of locals to also search for variables.
+ '''
+
+ def __init__(self, basedir, vars, globals, fail_on_undefined, *extras):
+ self.basedir = basedir
+ self.vars = vars
+ self.globals = globals
+ self.fail_on_undefined = fail_on_undefined
+ self.extras = extras
+
+ def __contains__(self, k):
+ if k in self.vars:
+ return True
+ for i in self.extras:
+ if k in i:
+ return True
+ if k in self.globals:
+ return True
+ return False
+
+ def __getitem__(self, varname):
+ from ansible.runner import HostVars
+ if varname not in self.vars:
+ for i in self.extras:
+ if varname in i:
+ return i[varname]
+ if varname in self.globals:
+ return self.globals[varname]
+ else:
+ raise KeyError("undefined variable: %s" % varname)
+ var = self.vars[varname]
+ # HostVars is special, return it as-is, as is the special variable
+ # 'vars', which contains the vars structure
+ var = to_unicode(var, nonstring="passthru")
+ if isinstance(var, dict) and varname == "vars" or isinstance(var, HostVars):
+ return var
+ else:
+ return template(self.basedir, var, self.vars, fail_on_undefined=self.fail_on_undefined)
+
+ def add_locals(self, locals):
+ '''
+ If locals are provided, create a copy of self containing those
+ locals in addition to what is already in this variable proxy.
+ '''
+ if locals is None:
+ return self
+ return _jinja2_vars(self.basedir, self.vars, self.globals, self.fail_on_undefined, locals, *self.extras)
+
+class J2Template(jinja2.environment.Template):
+ '''
+ This class prevents Jinja2 from running _jinja2_vars through dict()
+ Without this, {% include %} and similar will create new contexts unlike
+ the special one created in template_from_file. This ensures they are all
+ alike, except for potential locals.
+ '''
+ def new_context(self, vars=None, shared=False, locals=None):
+ return jinja2.runtime.Context(self.environment, vars.add_locals(locals), self.name, self.blocks)
+
+def template_from_file(basedir, path, vars, vault_password=None):
+ ''' run a file through the templating engine '''
+
+ fail_on_undefined = C.DEFAULT_UNDEFINED_VAR_BEHAVIOR
+
+ from ansible import utils
+ realpath = utils.path_dwim(basedir, path)
+ loader=jinja2.FileSystemLoader([basedir,os.path.dirname(realpath)])
+
+ def my_lookup(*args, **kwargs):
+ kwargs['vars'] = vars
+ return lookup(*args, basedir=basedir, **kwargs)
+ def my_finalize(thing):
+ return thing if thing is not None else ''
+
+ environment = jinja2.Environment(loader=loader, trim_blocks=True, extensions=_get_extensions())
+ environment.filters.update(_get_filters())
+ environment.globals['lookup'] = my_lookup
+ environment.globals['finalize'] = my_finalize
+ if fail_on_undefined:
+ environment.undefined = StrictUndefined
+
+ try:
+ data = codecs.open(realpath, encoding="utf8").read()
+ except UnicodeDecodeError:
+ raise errors.AnsibleError("unable to process as utf-8: %s" % realpath)
+ except:
+ raise errors.AnsibleError("unable to read %s" % realpath)
+
+ # 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()
+ if key in JINJA2_ALLOWED_OVERRIDES:
+ setattr(environment, key, ast.literal_eval(val.strip()))
+
+
+ environment.template_class = J2Template
+ try:
+ t = environment.from_string(data)
+ except TemplateSyntaxError, e:
+ # Throw an exception which includes a more user friendly error message
+ values = {'name': realpath, 'lineno': e.lineno, 'error': str(e)}
+ msg = 'file: %(name)s, line number: %(lineno)s, error: %(error)s' % \
+ values
+ error = errors.AnsibleError(msg)
+ raise error
+ vars = vars.copy()
+ try:
+ template_uid = pwd.getpwuid(os.stat(realpath).st_uid).pw_name
+ except:
+ template_uid = os.stat(realpath).st_uid
+ vars['template_host'] = os.uname()[1]
+ vars['template_path'] = realpath
+ vars['template_mtime'] = datetime.datetime.fromtimestamp(os.path.getmtime(realpath))
+ vars['template_uid'] = template_uid
+ vars['template_fullpath'] = os.path.abspath(realpath)
+ vars['template_run_date'] = datetime.datetime.now()
+
+ managed_default = C.DEFAULT_MANAGED_STR
+ managed_str = managed_default.format(
+ host = vars['template_host'],
+ uid = vars['template_uid'],
+ file = to_bytes(vars['template_path'])
+ )
+ vars['ansible_managed'] = time.strftime(
+ managed_str,
+ time.localtime(os.path.getmtime(realpath))
+ )
+
+ # This line performs deep Jinja2 magic that uses the _jinja2_vars object for vars
+ # Ideally, this could use some API where setting shared=True and the object won't get
+ # passed through dict(o), but I have not found that yet.
+ try:
+ res = jinja2.utils.concat(t.root_render_func(t.new_context(_jinja2_vars(basedir, vars, t.globals, fail_on_undefined), shared=True)))
+ except jinja2.exceptions.UndefinedError, e:
+ raise errors.AnsibleUndefinedVariable("One or more undefined variables: %s" % str(e))
+ except jinja2.exceptions.TemplateNotFound, e:
+ # Throw an exception which includes a more user friendly error message
+ # This likely will happen for included sub-template. Not that besides
+ # pure "file not found" it may happen due to Jinja2's "security"
+ # checks on path.
+ values = {'name': realpath, 'subname': str(e)}
+ msg = 'file: %(name)s, error: Cannot find/not allowed to load (include) template %(subname)s' % \
+ values
+ error = errors.AnsibleError(msg)
+ raise error
+
+ # 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
+ res_newlines = count_newlines_from_end(res)
+ data_newlines = count_newlines_from_end(data)
+ if data_newlines > res_newlines:
+ res += '\n' * (data_newlines - res_newlines)
+
+ if isinstance(res, unicode):
+ # do not try to re-template a unicode string
+ result = res
+ else:
+ result = template(basedir, res, vars)
+
+ return result
+
+def template_from_string(basedir, data, vars, fail_on_undefined=False):
+ ''' run a string through the (Jinja2) templating engine '''
+ try:
+ if type(data) == str:
+ data = unicode(data, 'utf-8')
+
+ # Check to see if the string we are trying to render is just referencing a single
+ # var. In this case we don't want to accidentally change the type of the variable
+ # to a string by using the jinja template renderer. We just want to pass it.
+ only_one = SINGLE_VAR.match(data)
+ if only_one:
+ var_name = only_one.group(1)
+ if var_name in vars:
+ resolved_val = vars[var_name]
+ if isinstance(resolved_val, (bool, Number)):
+ return resolved_val
+
+ def my_finalize(thing):
+ return thing if thing is not None else ''
+
+ environment = jinja2.Environment(trim_blocks=True, undefined=StrictUndefined, extensions=_get_extensions(), finalize=my_finalize)
+ environment.filters.update(_get_filters())
+ environment.template_class = J2Template
+
+ if '_original_file' in vars:
+ basedir = os.path.dirname(vars['_original_file'])
+ filesdir = os.path.abspath(os.path.join(basedir, '..', 'files'))
+ if os.path.exists(filesdir):
+ basedir = filesdir
+
+ # 6227
+ if isinstance(data, unicode):
+ try:
+ data = data.decode('utf-8')
+ except UnicodeEncodeError, e:
+ pass
+
+ try:
+ t = environment.from_string(data)
+ except TemplateSyntaxError, e:
+ raise errors.AnsibleError("template error while templating string: %s" % str(e))
+ except Exception, e:
+ if 'recursion' in str(e):
+ raise errors.AnsibleError("recursive loop detected in template string: %s" % data)
+ else:
+ return data
+
+ def my_lookup(*args, **kwargs):
+ kwargs['vars'] = vars
+ return lookup(*args, basedir=basedir, **kwargs)
+
+ t.globals['lookup'] = my_lookup
+ t.globals['finalize'] = my_finalize
+ jvars =_jinja2_vars(basedir, vars, t.globals, fail_on_undefined)
+ new_context = t.new_context(jvars, shared=True)
+ rf = t.root_render_func(new_context)
+ try:
+ res = jinja2.utils.concat(rf)
+ except TypeError, te:
+ if 'StrictUndefined' in str(te):
+ raise errors.AnsibleUndefinedVariable(
+ "Unable to look up a name or access an attribute in template string. " + \
+ "Make sure your variable name does not contain invalid characters like '-'."
+ )
+ else:
+ raise errors.AnsibleError("an unexpected type error occurred. Error was %s" % te)
+ return res
+ except (jinja2.exceptions.UndefinedError, errors.AnsibleUndefinedVariable):
+ if fail_on_undefined:
+ raise
+ else:
+ return data
+