From 0abcebf1e4763a7e3a1f81b1c8ea5a195de55064 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Sat, 14 Mar 2015 16:26:48 -0400 Subject: [PATCH] Don't convert numbers and booleans to strings. Before this change if a variable was of type int or bool and the variable was referenced by another variable, the type would change to string. eg. defaults/main.yml ``` PORT: 4567 OTHER_CONFIG: secret1: "so_secret" secret2: "even_more_secret" CONFIG: hostname: "some_hostname" port: "{{ PORT }}" secrets: "{{ OTHER_CONFIG }}" ``` If you output `CONFIG` to json or yaml, the port would get represented in the output as a string instead of as a number, but secrets would get represented as a dictionary. This is a mis-match in behaviour where some "types" are retained and others are not. This change should fix the issue. Update template test to also test var retainment. Make the template changes in v2. Update to only short-circuit for booleans and numbers. Added an entry to the changelog. --- CHANGELOG.md | 5 +++- lib/ansible/utils/template.py | 30 +++++++++++++++---- .../roles/test_template/files/foo.txt | 7 +++++ .../roles/test_template/templates/foo.j2 | 2 ++ .../roles/test_template/vars/main.yml | 13 ++++++++ v2/ansible/template/__init__.py | 21 +++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06fe0504fc7..69d7c3fd56a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ Ansible Changes By Release ## 2.0 "TBD" - ACTIVE DEVELOPMENT Major Changes: - big_ip modules now support turning off ssl certificate validation (use only for self signed) + - big_ip modules now support turning off ssl certificate validation (use only for self signed) + + - template code now retains types for bools and Numbers instead of turning them into strings + - If you need the old behaviour, quote the value and it will get passed around as a string New Modules: cloudtrail diff --git a/lib/ansible/utils/template.py b/lib/ansible/utils/template.py index 9426e254eb5..5f712b2675e 100644 --- a/lib/ansible/utils/template.py +++ b/lib/ansible/utils/template.py @@ -31,6 +31,7 @@ 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 @@ -81,6 +82,11 @@ class Flags: 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'] @@ -109,7 +115,6 @@ def lookup(name, *args, **kwargs): 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] @@ -123,10 +128,13 @@ def template(basedir, varname, templatevars, lookup_fatal=True, depth=0, expand_ except errors.AnsibleError, e: raise errors.AnsibleError("Failed to template %s: %s" % (varname, str(e))) - 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] + # 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 @@ -323,10 +331,20 @@ def template_from_file(basedir, path, vars, vault_password=None): 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 wont 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 '' diff --git a/test/integration/roles/test_template/files/foo.txt b/test/integration/roles/test_template/files/foo.txt index 3e96db9b3ec..edd704da048 100644 --- a/test/integration/roles/test_template/files/foo.txt +++ b/test/integration/roles/test_template/files/foo.txt @@ -1 +1,8 @@ templated_var_loaded + +{ + "bool": true, + "multi_part": "1Foo", + "number": 5, + "string_num": "5" +} diff --git a/test/integration/roles/test_template/templates/foo.j2 b/test/integration/roles/test_template/templates/foo.j2 index 55aab8f1ea1..22187f91300 100644 --- a/test/integration/roles/test_template/templates/foo.j2 +++ b/test/integration/roles/test_template/templates/foo.j2 @@ -1 +1,3 @@ {{ templated_var }} + +{{ templated_dict | to_nice_json }} diff --git a/test/integration/roles/test_template/vars/main.yml b/test/integration/roles/test_template/vars/main.yml index 1e8f64ccf44..b79f95e6cf1 100644 --- a/test/integration/roles/test_template/vars/main.yml +++ b/test/integration/roles/test_template/vars/main.yml @@ -1 +1,14 @@ templated_var: templated_var_loaded + +number_var: 5 +string_num: "5" +bool_var: true +part_1: 1 +part_2: "Foo" + +templated_dict: + number: "{{ number_var }}" + string_num: "{{ string_num }}" + bool: "{{ bool_var }}" + multi_part: "{{ part_1 }}{{ part_2 }}" + diff --git a/v2/ansible/template/__init__.py b/v2/ansible/template/__init__.py index 46bbc06a07d..0345a750081 100644 --- a/v2/ansible/template/__init__.py +++ b/v2/ansible/template/__init__.py @@ -32,8 +32,17 @@ from ansible.template.template import AnsibleJ2Template from ansible.template.vars import AnsibleJ2Vars from ansible.utils.debug import debug +from numbers import Number + __all__ = ['Templar'] +# 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*}}$") + +# Primitive Types which we don't want Jinja to convert to strings. +NON_TEMPLATED_TYPES = ( bool, Number ) + JINJA2_OVERRIDE = '#jinja2:' JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline'] @@ -125,6 +134,18 @@ class Templar: if isinstance(variable, basestring): result = variable if self._contains_vars(variable): + + # Check to see if the string we are trying to render is just referencing a single + # var. In this case we don't wont 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(variable) + if only_one: + var_name = only_one.group(1) + if var_name in self._available_vars: + resolved_val = self._available_vars[var_name] + if isinstance(resolved_val, NON_TEMPLATED_TYPES): + return resolved_val + result = self._do_template(variable, preserve_trailing_newlines=preserve_trailing_newlines) # if this looks like a dictionary or list, convert it to such using the safe_eval method