Several var fixes

* Fixes hostvar serialization issue (#12005)
* Fixes regression in include_vars from within a role (#9498), where
  we had the precedence order for vars_cache (include_vars, set_fact)
  incorrectly before role vars.
* Fixes another bug in which vars loaded from files in the format of
  a list instead of dictionary would cause a failure.

Fixes #9498
Fixes #12005
This commit is contained in:
James Cammarata 2015-08-21 10:59:32 -04:00
parent 144da7e7d1
commit 635fa0757b
2 changed files with 83 additions and 25 deletions

View file

@ -84,6 +84,26 @@ class VariableManager:
def set_inventory(self, inventory): def set_inventory(self, inventory):
self._inventory = inventory self._inventory = inventory
def _preprocess_vars(self, a):
'''
Ensures that vars contained in the parameter passed in are
returned as a list of dictionaries, to ensure for instance
that vars loaded from a file conform to an expected state.
'''
if a is None:
return None
elif not isinstance(a, list):
data = [ a ]
else:
data = a
for item in data:
if not isinstance(item, MutableMapping):
raise AnsibleError("variable files must contain either a dictionary of variables, or a list of dictionaries. Got: %s (%s)" % (a, type(a)))
return data
def _validate_both_dicts(self, a, b): def _validate_both_dicts(self, a, b):
''' '''
Validates that both arguments are dictionaries, or an error is raised. Validates that both arguments are dictionaries, or an error is raised.
@ -127,7 +147,7 @@ class VariableManager:
return result return result
def get_vars(self, loader, play=None, host=None, task=None, use_cache=True): def get_vars(self, loader, play=None, host=None, task=None, include_hostvars=True, use_cache=True):
''' '''
Returns the variables, with optional "context" given via the parameters Returns the variables, with optional "context" given via the parameters
for the play, host, and task (which could possibly result in different for the play, host, and task (which could possibly result in different
@ -168,16 +188,22 @@ class VariableManager:
# we merge in the special 'all' group_vars first, if they exist # we merge in the special 'all' group_vars first, if they exist
if 'all' in self._group_vars_files: if 'all' in self._group_vars_files:
all_vars = self._combine_vars(all_vars, self._group_vars_files['all']) data = self._preprocess_vars(self._group_vars_files['all'])
for item in data:
all_vars = self._combine_vars(all_vars, item)
for group in host.get_groups(): for group in host.get_groups():
all_vars = self._combine_vars(all_vars, group.get_vars()) all_vars = self._combine_vars(all_vars, group.get_vars())
if group.name in self._group_vars_files and group.name != 'all': if group.name in self._group_vars_files and group.name != 'all':
all_vars = self._combine_vars(all_vars, self._group_vars_files[group.name]) data = self._preprocess_vars(self._group_vars_files[group.name])
for item in data:
all_vars = self._combine_vars(all_vars, item)
host_name = host.get_name() host_name = host.get_name()
if host_name in self._host_vars_files: if host_name in self._host_vars_files:
all_vars = self._combine_vars(all_vars, self._host_vars_files[host_name]) data = self._preprocess_vars(self._host_vars_files[host_name])
for item in data:
all_vars = self._combine_vars(all_vars, self._host_vars_files[host_name])
# then we merge in vars specified for this host # then we merge in vars specified for this host
all_vars = self._combine_vars(all_vars, host.get_vars()) all_vars = self._combine_vars(all_vars, host.get_vars())
@ -209,9 +235,10 @@ class VariableManager:
# as soon as we read one from the list. If none are found, we # as soon as we read one from the list. If none are found, we
# raise an error, which is silently ignored at this point. # raise an error, which is silently ignored at this point.
for vars_file in vars_file_list: for vars_file in vars_file_list:
data = loader.load_from_file(vars_file) data = self._preprocess_vars(loader.load_from_file(vars_file))
if data is not None: if data is not None:
all_vars = self._combine_vars(all_vars, data) for item in data:
all_vars = self._combine_vars(all_vars, item)
break break
else: else:
raise AnsibleError("vars file %s was not found" % vars_file_item) raise AnsibleError("vars file %s was not found" % vars_file_item)
@ -222,14 +249,14 @@ class VariableManager:
for role in play.get_roles(): for role in play.get_roles():
all_vars = self._combine_vars(all_vars, role.get_vars()) all_vars = self._combine_vars(all_vars, role.get_vars())
if host:
all_vars = self._combine_vars(all_vars, self._vars_cache.get(host.get_name(), dict()))
if task: if task:
if task._role: if task._role:
all_vars = self._combine_vars(all_vars, task._role.get_vars()) all_vars = self._combine_vars(all_vars, task._role.get_vars())
all_vars = self._combine_vars(all_vars, task.get_vars()) all_vars = self._combine_vars(all_vars, task.get_vars())
if host:
all_vars = self._combine_vars(all_vars, self._vars_cache.get(host.get_name(), dict()))
all_vars = self._combine_vars(all_vars, self._extra_vars) all_vars = self._combine_vars(all_vars, self._extra_vars)
# FIXME: make sure all special vars are here # FIXME: make sure all special vars are here
@ -241,9 +268,10 @@ class VariableManager:
all_vars['groups'] = [group.name for group in host.get_groups()] all_vars['groups'] = [group.name for group in host.get_groups()]
if self._inventory is not None: if self._inventory is not None:
hostvars = HostVars(vars_manager=self, play=play, inventory=self._inventory, loader=loader)
all_vars['hostvars'] = hostvars
all_vars['groups'] = self._inventory.groups_list() all_vars['groups'] = self._inventory.groups_list()
if include_hostvars:
hostvars = HostVars(vars_manager=self, play=play, inventory=self._inventory, loader=loader)
all_vars['hostvars'] = hostvars
if task: if task:
if task._role: if task._role:

View file

@ -20,9 +20,12 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import collections import collections
import sys
from jinja2 import Undefined as j2undefined from jinja2 import Undefined as j2undefined
from ansible import constants as C
from ansible.inventory.host import Host
from ansible.template import Templar from ansible.template import Templar
__all__ = ['HostVars'] __all__ = ['HostVars']
@ -32,22 +35,47 @@ class HostVars(collections.Mapping):
''' A special view of vars_cache that adds values from the inventory when needed. ''' ''' A special view of vars_cache that adds values from the inventory when needed. '''
def __init__(self, vars_manager, play, inventory, loader): def __init__(self, vars_manager, play, inventory, loader):
self._vars_manager = vars_manager self._lookup = {}
self._play = play self._loader = loader
self._inventory = inventory
self._loader = loader # temporarily remove the inventory filter restriction
self._lookup = {} # so we can compile the variables for all of the hosts
# in inventory
restriction = inventory._restriction
inventory.remove_restriction()
hosts = inventory.get_hosts()
inventory.restrict_to_hosts(restriction)
# check to see if localhost is in the hosts list, as we
# may have it referenced via hostvars but if created implicitly
# it doesn't sow up in the hosts list
has_localhost = False
for host in hosts:
if host.name in C.LOCALHOST:
has_localhost = True
break
# we don't use the method in inventory to create the implicit host,
# because it also adds it to the 'ungrouped' group, and we want to
# avoid any side-effects
if not has_localhost:
new_host = Host(name='localhost')
new_host.set_variable("ansible_python_interpreter", sys.executable)
new_host.set_variable("ansible_connection", "local")
new_host.ipv4_address = '127.0.0.1'
hosts.append(new_host)
for host in hosts:
self._lookup[host.name] = vars_manager.get_vars(loader=loader, play=play, host=host, include_hostvars=False)
def __getitem__(self, host_name): def __getitem__(self, host_name):
if host_name not in self._lookup: if host_name not in self._lookup:
host = self._inventory.get_host(host_name) return j2undefined
if not host:
return j2undefined data = self._lookup.get(host_name)
result = self._vars_manager.get_vars(loader=self._loader, play=self._play, host=host) templar = Templar(variables=data, loader=self._loader)
templar = Templar(variables=result, loader=self._loader) return templar.template(data, fail_on_undefined=False)
self._lookup[host_name] = templar.template(result, fail_on_undefined=False)
return self._lookup[host_name]
def __contains__(self, host_name): def __contains__(self, host_name):
item = self.get(host_name) item = self.get(host_name)
@ -62,7 +90,9 @@ class HostVars(collections.Mapping):
raise NotImplementedError('HostVars does not support len. hosts entries are discovered dynamically as needed') raise NotImplementedError('HostVars does not support len. hosts entries are discovered dynamically as needed')
def __getstate__(self): def __getstate__(self):
return self._lookup data = self._lookup.copy()
return dict(loader=self._loader, data=data)
def __setstate__(self, data): def __setstate__(self, data):
self._lookup = data self._lookup = data.get('data')
self._loader = data.get('loader')