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.
This commit is contained in:
Feanil Patel 2015-03-14 16:26:48 -04:00
parent a6084f9fd8
commit 0abcebf1e4
6 changed files with 71 additions and 7 deletions

View file

@ -4,7 +4,10 @@ Ansible Changes By Release
## 2.0 "TBD" - ACTIVE DEVELOPMENT ## 2.0 "TBD" - ACTIVE DEVELOPMENT
Major Changes: 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: New Modules:
cloudtrail cloudtrail

View file

@ -31,6 +31,7 @@ import datetime
import pwd import pwd
import ast import ast
import traceback import traceback
from numbers import Number
from ansible.utils.string_functions import count_newlines_from_end from ansible.utils.string_functions import count_newlines_from_end
from ansible.utils import to_bytes, to_unicode from ansible.utils import to_bytes, to_unicode
@ -81,6 +82,11 @@ class Flags:
FILTER_PLUGINS = None FILTER_PLUGINS = None
_LISTRE = re.compile(r"(\w+)\[(\d+)\]") _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_OVERRIDE = '#jinja2:'
JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline'] 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): 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 ''' ''' templates a data structure by traversing it and substituting for other data structures '''
from ansible import utils from ansible import utils
try: try:
if convert_bare and isinstance(varname, basestring): if convert_bare and isinstance(varname, basestring):
first_part = varname.split(".")[0].split("[")[0] 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: except errors.AnsibleError, e:
raise errors.AnsibleError("Failed to template %s: %s" % (varname, str(e))) raise errors.AnsibleError("Failed to template %s: %s" % (varname, str(e)))
if (varname.startswith("{") and not varname.startswith("{{")) or varname.startswith("["): # template_from_string may return non strings for the case where the var is just
eval_results = utils.safe_eval(varname, locals=templatevars, include_exceptions=True) # a reference to a single variable, so we should re_check before we do further evals
if eval_results[1] is None: if isinstance(varname, basestring):
varname = eval_results[0] 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 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): def template_from_string(basedir, data, vars, fail_on_undefined=False):
''' run a string through the (Jinja2) templating engine ''' ''' run a string through the (Jinja2) templating engine '''
try: try:
if type(data) == str: if type(data) == str:
data = unicode(data, 'utf-8') 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): def my_finalize(thing):
return thing if thing is not None else '' return thing if thing is not None else ''

View file

@ -1 +1,8 @@
templated_var_loaded templated_var_loaded
{
"bool": true,
"multi_part": "1Foo",
"number": 5,
"string_num": "5"
}

View file

@ -1 +1,3 @@
{{ templated_var }} {{ templated_var }}
{{ templated_dict | to_nice_json }}

View file

@ -1 +1,14 @@
templated_var: templated_var_loaded 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 }}"

View file

@ -32,8 +32,17 @@ from ansible.template.template import AnsibleJ2Template
from ansible.template.vars import AnsibleJ2Vars from ansible.template.vars import AnsibleJ2Vars
from ansible.utils.debug import debug from ansible.utils.debug import debug
from numbers import Number
__all__ = ['Templar'] __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_OVERRIDE = '#jinja2:'
JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline'] JINJA2_ALLOWED_OVERRIDES = ['trim_blocks', 'lstrip_blocks', 'newline_sequence', 'keep_trailing_newline']
@ -125,6 +134,18 @@ class Templar:
if isinstance(variable, basestring): if isinstance(variable, basestring):
result = variable result = variable
if self._contains_vars(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) 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 # if this looks like a dictionary or list, convert it to such using the safe_eval method