1# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
2#
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17
18# Make coding more python3-ish
19from __future__ import (absolute_import, division, print_function)
20__metaclass__ = type
21
22import os
23import sys
24
25from collections import defaultdict
26
27try:
28    from hashlib import sha1
29except ImportError:
30    from sha import sha as sha1
31
32from jinja2.exceptions import UndefinedError
33
34from ansible import constants as C
35from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleFileNotFound, AnsibleAssertionError, AnsibleTemplateError
36from ansible.inventory.host import Host
37from ansible.inventory.helpers import sort_groups, get_group_vars
38from ansible.module_utils._text import to_text
39from ansible.module_utils.common._collections_compat import Mapping, MutableMapping, Sequence
40from ansible.module_utils.six import iteritems, text_type, string_types
41from ansible.plugins.loader import lookup_loader
42from ansible.vars.fact_cache import FactCache
43from ansible.template import Templar
44from ansible.utils.display import Display
45from ansible.utils.listify import listify_lookup_plugin_terms
46from ansible.utils.vars import combine_vars, load_extra_vars, load_options_vars
47from ansible.utils.unsafe_proxy import wrap_var
48from ansible.vars.clean import namespace_facts, clean_facts
49from ansible.vars.plugins import get_vars_from_inventory_sources, get_vars_from_path
50
51display = Display()
52
53
54def preprocess_vars(a):
55    '''
56    Ensures that vars contained in the parameter passed in are
57    returned as a list of dictionaries, to ensure for instance
58    that vars loaded from a file conform to an expected state.
59    '''
60
61    if a is None:
62        return None
63    elif not isinstance(a, list):
64        data = [a]
65    else:
66        data = a
67
68    for item in data:
69        if not isinstance(item, MutableMapping):
70            raise AnsibleError("variable files must contain either a dictionary of variables, or a list of dictionaries. Got: %s (%s)" % (a, type(a)))
71
72    return data
73
74
75class VariableManager:
76
77    _ALLOWED = frozenset(['plugins_by_group', 'groups_plugins_play', 'groups_plugins_inventory', 'groups_inventory',
78                          'all_plugins_play', 'all_plugins_inventory', 'all_inventory'])
79
80    def __init__(self, loader=None, inventory=None, version_info=None):
81        self._nonpersistent_fact_cache = defaultdict(dict)
82        self._vars_cache = defaultdict(dict)
83        self._extra_vars = defaultdict(dict)
84        self._host_vars_files = defaultdict(dict)
85        self._group_vars_files = defaultdict(dict)
86        self._inventory = inventory
87        self._loader = loader
88        self._hostvars = None
89        self._omit_token = '__omit_place_holder__%s' % sha1(os.urandom(64)).hexdigest()
90
91        self._options_vars = load_options_vars(version_info)
92
93        # If the basedir is specified as the empty string then it results in cwd being used.
94        # This is not a safe location to load vars from.
95        basedir = self._options_vars.get('basedir', False)
96        self.safe_basedir = bool(basedir is False or basedir)
97
98        # load extra vars
99        self._extra_vars = load_extra_vars(loader=self._loader)
100
101        # load fact cache
102        try:
103            self._fact_cache = FactCache()
104        except AnsibleError as e:
105            # bad cache plugin is not fatal error
106            # fallback to a dict as in memory cache
107            display.warning(to_text(e))
108            self._fact_cache = {}
109
110    def __getstate__(self):
111        data = dict(
112            fact_cache=self._fact_cache,
113            np_fact_cache=self._nonpersistent_fact_cache,
114            vars_cache=self._vars_cache,
115            extra_vars=self._extra_vars,
116            host_vars_files=self._host_vars_files,
117            group_vars_files=self._group_vars_files,
118            omit_token=self._omit_token,
119            options_vars=self._options_vars,
120            inventory=self._inventory,
121            safe_basedir=self.safe_basedir,
122        )
123        return data
124
125    def __setstate__(self, data):
126        self._fact_cache = data.get('fact_cache', defaultdict(dict))
127        self._nonpersistent_fact_cache = data.get('np_fact_cache', defaultdict(dict))
128        self._vars_cache = data.get('vars_cache', defaultdict(dict))
129        self._extra_vars = data.get('extra_vars', dict())
130        self._host_vars_files = data.get('host_vars_files', defaultdict(dict))
131        self._group_vars_files = data.get('group_vars_files', defaultdict(dict))
132        self._omit_token = data.get('omit_token', '__omit_place_holder__%s' % sha1(os.urandom(64)).hexdigest())
133        self._inventory = data.get('inventory', None)
134        self._options_vars = data.get('options_vars', dict())
135        self.safe_basedir = data.get('safe_basedir', False)
136        self._loader = None
137        self._hostvars = None
138
139    @property
140    def extra_vars(self):
141        return self._extra_vars
142
143    def set_inventory(self, inventory):
144        self._inventory = inventory
145
146    def get_vars(self, play=None, host=None, task=None, include_hostvars=True, include_delegate_to=True, use_cache=True,
147                 _hosts=None, _hosts_all=None, stage='task'):
148        '''
149        Returns the variables, with optional "context" given via the parameters
150        for the play, host, and task (which could possibly result in different
151        sets of variables being returned due to the additional context).
152
153        The order of precedence is:
154        - play->roles->get_default_vars (if there is a play context)
155        - group_vars_files[host] (if there is a host context)
156        - host_vars_files[host] (if there is a host context)
157        - host->get_vars (if there is a host context)
158        - fact_cache[host] (if there is a host context)
159        - play vars (if there is a play context)
160        - play vars_files (if there's no host context, ignore
161          file names that cannot be templated)
162        - task->get_vars (if there is a task context)
163        - vars_cache[host] (if there is a host context)
164        - extra vars
165
166        ``_hosts`` and ``_hosts_all`` should be considered private args, with only internal trusted callers relying
167        on the functionality they provide. These arguments may be removed at a later date without a deprecation
168        period and without warning.
169        '''
170
171        display.debug("in VariableManager get_vars()")
172
173        all_vars = dict()
174        magic_variables = self._get_magic_variables(
175            play=play,
176            host=host,
177            task=task,
178            include_hostvars=include_hostvars,
179            include_delegate_to=include_delegate_to,
180            _hosts=_hosts,
181            _hosts_all=_hosts_all,
182        )
183
184        _vars_sources = {}
185
186        def _combine_and_track(data, new_data, source):
187            '''
188            Wrapper function to update var sources dict and call combine_vars()
189
190            See notes in the VarsWithSources docstring for caveats and limitations of the source tracking
191            '''
192            if C.DEFAULT_DEBUG:
193                # Populate var sources dict
194                for key in new_data:
195                    _vars_sources[key] = source
196            return combine_vars(data, new_data)
197
198        # default for all cases
199        basedirs = []
200        if self.safe_basedir:  # avoid adhoc/console loading cwd
201            basedirs = [self._loader.get_basedir()]
202
203        if play:
204            # first we compile any vars specified in defaults/main.yml
205            # for all roles within the specified play
206            for role in play.get_roles():
207                all_vars = _combine_and_track(all_vars, role.get_default_vars(), "role '%s' defaults" % role.name)
208
209        if task:
210            # set basedirs
211            if C.PLAYBOOK_VARS_ROOT == 'all':  # should be default
212                basedirs = task.get_search_path()
213            elif C.PLAYBOOK_VARS_ROOT in ('bottom', 'playbook_dir'):  # only option in 2.4.0
214                basedirs = [task.get_search_path()[0]]
215            elif C.PLAYBOOK_VARS_ROOT != 'top':
216                # preserves default basedirs, only option pre 2.3
217                raise AnsibleError('Unknown playbook vars logic: %s' % C.PLAYBOOK_VARS_ROOT)
218
219            # if we have a task in this context, and that task has a role, make
220            # sure it sees its defaults above any other roles, as we previously
221            # (v1) made sure each task had a copy of its roles default vars
222            if task._role is not None and (play or task.action in C._ACTION_INCLUDE_ROLE):
223                all_vars = _combine_and_track(all_vars, task._role.get_default_vars(dep_chain=task.get_dep_chain()),
224                                              "role '%s' defaults" % task._role.name)
225
226        if host:
227            # THE 'all' group and the rest of groups for a host, used below
228            all_group = self._inventory.groups.get('all')
229            host_groups = sort_groups([g for g in host.get_groups() if g.name not in ['all']])
230
231            def _get_plugin_vars(plugin, path, entities):
232                data = {}
233                try:
234                    data = plugin.get_vars(self._loader, path, entities)
235                except AttributeError:
236                    try:
237                        for entity in entities:
238                            if isinstance(entity, Host):
239                                data.update(plugin.get_host_vars(entity.name))
240                            else:
241                                data.update(plugin.get_group_vars(entity.name))
242                    except AttributeError:
243                        if hasattr(plugin, 'run'):
244                            raise AnsibleError("Cannot use v1 type vars plugin %s from %s" % (plugin._load_name, plugin._original_path))
245                        else:
246                            raise AnsibleError("Invalid vars plugin %s from %s" % (plugin._load_name, plugin._original_path))
247                return data
248
249            # internal functions that actually do the work
250            def _plugins_inventory(entities):
251                ''' merges all entities by inventory source '''
252                return get_vars_from_inventory_sources(self._loader, self._inventory._sources, entities, stage)
253
254            def _plugins_play(entities):
255                ''' merges all entities adjacent to play '''
256                data = {}
257                for path in basedirs:
258                    data = _combine_and_track(data, get_vars_from_path(self._loader, path, entities, stage), "path '%s'" % path)
259                return data
260
261            # configurable functions that are sortable via config, remember to add to _ALLOWED if expanding this list
262            def all_inventory():
263                return all_group.get_vars()
264
265            def all_plugins_inventory():
266                return _plugins_inventory([all_group])
267
268            def all_plugins_play():
269                return _plugins_play([all_group])
270
271            def groups_inventory():
272                ''' gets group vars from inventory '''
273                return get_group_vars(host_groups)
274
275            def groups_plugins_inventory():
276                ''' gets plugin sources from inventory for groups '''
277                return _plugins_inventory(host_groups)
278
279            def groups_plugins_play():
280                ''' gets plugin sources from play for groups '''
281                return _plugins_play(host_groups)
282
283            def plugins_by_groups():
284                '''
285                    merges all plugin sources by group,
286                    This should be used instead, NOT in combination with the other groups_plugins* functions
287                '''
288                data = {}
289                for group in host_groups:
290                    data[group] = _combine_and_track(data[group], _plugins_inventory(group), "inventory group_vars for '%s'" % group)
291                    data[group] = _combine_and_track(data[group], _plugins_play(group), "playbook group_vars for '%s'" % group)
292                return data
293
294            # Merge groups as per precedence config
295            # only allow to call the functions we want exposed
296            for entry in C.VARIABLE_PRECEDENCE:
297                if entry in self._ALLOWED:
298                    display.debug('Calling %s to load vars for %s' % (entry, host.name))
299                    all_vars = _combine_and_track(all_vars, locals()[entry](), "group vars, precedence entry '%s'" % entry)
300                else:
301                    display.warning('Ignoring unknown variable precedence entry: %s' % (entry))
302
303            # host vars, from inventory, inventory adjacent and play adjacent via plugins
304            all_vars = _combine_and_track(all_vars, host.get_vars(), "host vars for '%s'" % host)
305            all_vars = _combine_and_track(all_vars, _plugins_inventory([host]), "inventory host_vars for '%s'" % host)
306            all_vars = _combine_and_track(all_vars, _plugins_play([host]), "playbook host_vars for '%s'" % host)
307
308            # finally, the facts caches for this host, if it exists
309            # TODO: cleaning of facts should eventually become part of taskresults instead of vars
310            try:
311                facts = wrap_var(self._fact_cache.get(host.name, {}))
312                all_vars.update(namespace_facts(facts))
313
314                # push facts to main namespace
315                if C.INJECT_FACTS_AS_VARS:
316                    all_vars = _combine_and_track(all_vars, wrap_var(clean_facts(facts)), "facts")
317                else:
318                    # always 'promote' ansible_local
319                    all_vars = _combine_and_track(all_vars, wrap_var({'ansible_local': facts.get('ansible_local', {})}), "facts")
320            except KeyError:
321                pass
322
323        if play:
324            all_vars = _combine_and_track(all_vars, play.get_vars(), "play vars")
325
326            vars_files = play.get_vars_files()
327            try:
328                for vars_file_item in vars_files:
329                    # create a set of temporary vars here, which incorporate the extra
330                    # and magic vars so we can properly template the vars_files entries
331                    # NOTE: this makes them depend on host vars/facts so things like
332                    #       ansible_facts['os_distribution'] can be used, ala include_vars.
333                    #       Consider DEPRECATING this in the future, since we have include_vars ...
334                    temp_vars = combine_vars(all_vars, self._extra_vars)
335                    temp_vars = combine_vars(temp_vars, magic_variables)
336                    templar = Templar(loader=self._loader, variables=temp_vars)
337
338                    # we assume each item in the list is itself a list, as we
339                    # support "conditional includes" for vars_files, which mimics
340                    # the with_first_found mechanism.
341                    vars_file_list = vars_file_item
342                    if not isinstance(vars_file_list, list):
343                        vars_file_list = [vars_file_list]
344
345                    # now we iterate through the (potential) files, and break out
346                    # as soon as we read one from the list. If none are found, we
347                    # raise an error, which is silently ignored at this point.
348                    try:
349                        for vars_file in vars_file_list:
350                            vars_file = templar.template(vars_file)
351                            if not (isinstance(vars_file, Sequence)):
352                                raise AnsibleError(
353                                    "Invalid vars_files entry found: %r\n"
354                                    "vars_files entries should be either a string type or "
355                                    "a list of string types after template expansion" % vars_file
356                                )
357                            try:
358                                data = preprocess_vars(self._loader.load_from_file(vars_file, unsafe=True))
359                                if data is not None:
360                                    for item in data:
361                                        all_vars = _combine_and_track(all_vars, item, "play vars_files from '%s'" % vars_file)
362                                break
363                            except AnsibleFileNotFound:
364                                # we continue on loader failures
365                                continue
366                            except AnsibleParserError:
367                                raise
368                        else:
369                            # if include_delegate_to is set to False or we don't have a host, we ignore the missing
370                            # vars file here because we're working on a delegated host or require host vars, see NOTE above
371                            if include_delegate_to and host:
372                                raise AnsibleFileNotFound("vars file %s was not found" % vars_file_item)
373                    except (UndefinedError, AnsibleUndefinedVariable):
374                        if host is not None and self._fact_cache.get(host.name, dict()).get('module_setup') and task is not None:
375                            raise AnsibleUndefinedVariable("an undefined variable was found when attempting to template the vars_files item '%s'"
376                                                           % vars_file_item, obj=vars_file_item)
377                        else:
378                            # we do not have a full context here, and the missing variable could be because of that
379                            # so just show a warning and continue
380                            display.vvv("skipping vars_file '%s' due to an undefined variable" % vars_file_item)
381                            continue
382
383                    display.vvv("Read vars_file '%s'" % vars_file_item)
384            except TypeError:
385                raise AnsibleParserError("Error while reading vars files - please supply a list of file names. "
386                                         "Got '%s' of type %s" % (vars_files, type(vars_files)))
387
388            # By default, we now merge in all vars from all roles in the play,
389            # unless the user has disabled this via a config option
390            if not C.DEFAULT_PRIVATE_ROLE_VARS:
391                for role in play.get_roles():
392                    all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False), "role '%s' vars" % role.name)
393
394        # next, we merge in the vars from the role, which will specifically
395        # follow the role dependency chain, and then we merge in the tasks
396        # vars (which will look at parent blocks/task includes)
397        if task:
398            if task._role:
399                all_vars = _combine_and_track(all_vars, task._role.get_vars(task.get_dep_chain(), include_params=False),
400                                              "role '%s' vars" % task._role.name)
401            all_vars = _combine_and_track(all_vars, task.get_vars(), "task vars")
402
403        # next, we merge in the vars cache (include vars) and nonpersistent
404        # facts cache (set_fact/register), in that order
405        if host:
406            # include_vars non-persistent cache
407            all_vars = _combine_and_track(all_vars, self._vars_cache.get(host.get_name(), dict()), "include_vars")
408            # fact non-persistent cache
409            all_vars = _combine_and_track(all_vars, self._nonpersistent_fact_cache.get(host.name, dict()), "set_fact")
410
411        # next, we merge in role params and task include params
412        if task:
413            if task._role:
414                all_vars = _combine_and_track(all_vars, task._role.get_role_params(task.get_dep_chain()), "role '%s' params" % task._role.name)
415
416            # special case for include tasks, where the include params
417            # may be specified in the vars field for the task, which should
418            # have higher precedence than the vars/np facts above
419            all_vars = _combine_and_track(all_vars, task.get_include_params(), "include params")
420
421        # extra vars
422        all_vars = _combine_and_track(all_vars, self._extra_vars, "extra vars")
423
424        # magic variables
425        all_vars = _combine_and_track(all_vars, magic_variables, "magic vars")
426
427        # special case for the 'environment' magic variable, as someone
428        # may have set it as a variable and we don't want to stomp on it
429        if task:
430            all_vars['environment'] = task.environment
431
432        # 'vars' magic var
433        if task or play:
434            # has to be copy, otherwise recursive ref
435            all_vars['vars'] = all_vars.copy()
436
437        # if we have a host and task and we're delegating to another host,
438        # figure out the variables for that host now so we don't have to rely on host vars later
439        if task and host and task.delegate_to is not None and include_delegate_to:
440            all_vars['ansible_delegated_vars'], all_vars['_ansible_loop_cache'] = self._get_delegated_vars(play, task, all_vars)
441
442        display.debug("done with get_vars()")
443        if C.DEFAULT_DEBUG:
444            # Use VarsWithSources wrapper class to display var sources
445            return VarsWithSources.new_vars_with_sources(all_vars, _vars_sources)
446        else:
447            return all_vars
448
449    def _get_magic_variables(self, play, host, task, include_hostvars, include_delegate_to, _hosts=None, _hosts_all=None):
450        '''
451        Returns a dictionary of so-called "magic" variables in Ansible,
452        which are special variables we set internally for use.
453        '''
454
455        variables = {}
456        variables['playbook_dir'] = os.path.abspath(self._loader.get_basedir())
457        variables['ansible_playbook_python'] = sys.executable
458        variables['ansible_config_file'] = C.CONFIG_FILE
459
460        if play:
461            # This is a list of all role names of all dependencies for all roles for this play
462            dependency_role_names = list(set([d.get_name() for r in play.roles for d in r.get_all_dependencies()]))
463            # This is a list of all role names of all roles for this play
464            play_role_names = [r.get_name() for r in play.roles]
465
466            # ansible_role_names includes all role names, dependent or directly referenced by the play
467            variables['ansible_role_names'] = list(set(dependency_role_names + play_role_names))
468            # ansible_play_role_names includes the names of all roles directly referenced by this play
469            # roles that are implicitly referenced via dependencies are not listed.
470            variables['ansible_play_role_names'] = play_role_names
471            # ansible_dependent_role_names includes the names of all roles that are referenced via dependencies
472            # dependencies that are also explicitly named as roles are included in this list
473            variables['ansible_dependent_role_names'] = dependency_role_names
474
475            # DEPRECATED: role_names should be deprecated in favor of ansible_role_names or ansible_play_role_names
476            variables['role_names'] = variables['ansible_play_role_names']
477
478            variables['ansible_play_name'] = play.get_name()
479
480        if task:
481            if task._role:
482                variables['role_name'] = task._role.get_name(include_role_fqcn=False)
483                variables['role_path'] = task._role._role_path
484                variables['role_uuid'] = text_type(task._role._uuid)
485                variables['ansible_collection_name'] = task._role._role_collection
486                variables['ansible_role_name'] = task._role.get_name()
487
488        if self._inventory is not None:
489            variables['groups'] = self._inventory.get_groups_dict()
490            if play:
491                templar = Templar(loader=self._loader)
492                if templar.is_template(play.hosts):
493                    pattern = 'all'
494                else:
495                    pattern = play.hosts or 'all'
496                # add the list of hosts in the play, as adjusted for limit/filters
497                if not _hosts_all:
498                    _hosts_all = [h.name for h in self._inventory.get_hosts(pattern=pattern, ignore_restrictions=True)]
499                if not _hosts:
500                    _hosts = [h.name for h in self._inventory.get_hosts()]
501
502                variables['ansible_play_hosts_all'] = _hosts_all[:]
503                variables['ansible_play_hosts'] = [x for x in variables['ansible_play_hosts_all'] if x not in play._removed_hosts]
504                variables['ansible_play_batch'] = [x for x in _hosts if x not in play._removed_hosts]
505
506                # DEPRECATED: play_hosts should be deprecated in favor of ansible_play_batch,
507                # however this would take work in the templating engine, so for now we'll add both
508                variables['play_hosts'] = variables['ansible_play_batch']
509
510        # the 'omit' value allows params to be left out if the variable they are based on is undefined
511        variables['omit'] = self._omit_token
512        # Set options vars
513        for option, option_value in iteritems(self._options_vars):
514            variables[option] = option_value
515
516        if self._hostvars is not None and include_hostvars:
517            variables['hostvars'] = self._hostvars
518
519        return variables
520
521    def _get_delegated_vars(self, play, task, existing_variables):
522        # This method has a lot of code copied from ``TaskExecutor._get_loop_items``
523        # if this is failing, and ``TaskExecutor._get_loop_items`` is not
524        # then more will have to be copied here.
525        # TODO: dedupe code here and with ``TaskExecutor._get_loop_items``
526        #       this may be possible once we move pre-processing pre fork
527
528        if not hasattr(task, 'loop'):
529            # This "task" is not a Task, so we need to skip it
530            return {}, None
531
532        # we unfortunately need to template the delegate_to field here,
533        # as we're fetching vars before post_validate has been called on
534        # the task that has been passed in
535        vars_copy = existing_variables.copy()
536
537        # get search path for this task to pass to lookup plugins
538        vars_copy['ansible_search_path'] = task.get_search_path()
539
540        # ensure basedir is always in (dwim already searches here but we need to display it)
541        if self._loader.get_basedir() not in vars_copy['ansible_search_path']:
542            vars_copy['ansible_search_path'].append(self._loader.get_basedir())
543
544        templar = Templar(loader=self._loader, variables=vars_copy)
545
546        items = []
547        has_loop = True
548        if task.loop_with is not None:
549            if task.loop_with in lookup_loader:
550                fail = True
551                if task.loop_with == 'first_found':
552                    # first_found loops are special. If the item is undefined then we want to fall through to the next
553                    fail = False
554                try:
555                    loop_terms = listify_lookup_plugin_terms(terms=task.loop, templar=templar,
556                                                             loader=self._loader, fail_on_undefined=fail, convert_bare=False)
557
558                    if not fail:
559                        loop_terms = [t for t in loop_terms if not templar.is_template(t)]
560
561                    mylookup = lookup_loader.get(task.loop_with, loader=self._loader, templar=templar)
562
563                    # give lookup task 'context' for subdir (mostly needed for first_found)
564                    for subdir in ['template', 'var', 'file']:  # TODO: move this to constants?
565                        if subdir in task.action:
566                            break
567                    setattr(mylookup, '_subdir', subdir + 's')
568
569                    items = wrap_var(mylookup.run(terms=loop_terms, variables=vars_copy))
570
571                except AnsibleTemplateError:
572                    # This task will be skipped later due to this, so we just setup
573                    # a dummy array for the later code so it doesn't fail
574                    items = [None]
575            else:
576                raise AnsibleError("Failed to find the lookup named '%s' in the available lookup plugins" % task.loop_with)
577        elif task.loop is not None:
578            try:
579                items = templar.template(task.loop)
580            except AnsibleTemplateError:
581                # This task will be skipped later due to this, so we just setup
582                # a dummy array for the later code so it doesn't fail
583                items = [None]
584        else:
585            has_loop = False
586            items = [None]
587
588        # since host can change per loop, we keep dict per host name resolved
589        delegated_host_vars = dict()
590        item_var = getattr(task.loop_control, 'loop_var', 'item')
591        cache_items = False
592        for item in items:
593            # update the variables with the item value for templating, in case we need it
594            if item is not None:
595                vars_copy[item_var] = item
596
597            templar.available_variables = vars_copy
598            delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False)
599            if delegated_host_name != task.delegate_to:
600                cache_items = True
601            if delegated_host_name is None:
602                raise AnsibleError(message="Undefined delegate_to host for task:", obj=task._ds)
603            if not isinstance(delegated_host_name, string_types):
604                raise AnsibleError(message="the field 'delegate_to' has an invalid type (%s), and could not be"
605                                           " converted to a string type." % type(delegated_host_name), obj=task._ds)
606
607            if delegated_host_name in delegated_host_vars:
608                # no need to repeat ourselves, as the delegate_to value
609                # does not appear to be tied to the loop item variable
610                continue
611
612            # now try to find the delegated-to host in inventory, or failing that,
613            # create a new host on the fly so we can fetch variables for it
614            delegated_host = None
615            if self._inventory is not None:
616                delegated_host = self._inventory.get_host(delegated_host_name)
617                # try looking it up based on the address field, and finally
618                # fall back to creating a host on the fly to use for the var lookup
619                if delegated_host is None:
620                    for h in self._inventory.get_hosts(ignore_limits=True, ignore_restrictions=True):
621                        # check if the address matches, or if both the delegated_to host
622                        # and the current host are in the list of localhost aliases
623                        if h.address == delegated_host_name:
624                            delegated_host = h
625                            break
626                    else:
627                        delegated_host = Host(name=delegated_host_name)
628            else:
629                delegated_host = Host(name=delegated_host_name)
630
631            # now we go fetch the vars for the delegated-to host and save them in our
632            # master dictionary of variables to be used later in the TaskExecutor/PlayContext
633            delegated_host_vars[delegated_host_name] = self.get_vars(
634                play=play,
635                host=delegated_host,
636                task=task,
637                include_delegate_to=False,
638                include_hostvars=True,
639            )
640            delegated_host_vars[delegated_host_name]['inventory_hostname'] = vars_copy.get('inventory_hostname')
641
642        _ansible_loop_cache = None
643        if has_loop and cache_items:
644            # delegate_to templating produced a change, so we will cache the templated items
645            # in a special private hostvar
646            # this ensures that delegate_to+loop doesn't produce different results than TaskExecutor
647            # which may reprocess the loop
648            _ansible_loop_cache = items
649
650        return delegated_host_vars, _ansible_loop_cache
651
652    def clear_facts(self, hostname):
653        '''
654        Clears the facts for a host
655        '''
656        self._fact_cache.pop(hostname, None)
657
658    def set_host_facts(self, host, facts):
659        '''
660        Sets or updates the given facts for a host in the fact cache.
661        '''
662
663        if not isinstance(facts, Mapping):
664            raise AnsibleAssertionError("the type of 'facts' to set for host_facts should be a Mapping but is a %s" % type(facts))
665
666        try:
667            host_cache = self._fact_cache[host]
668        except KeyError:
669            # We get to set this as new
670            host_cache = facts
671        else:
672            if not isinstance(host_cache, MutableMapping):
673                raise TypeError('The object retrieved for {0} must be a MutableMapping but was'
674                                ' a {1}'.format(host, type(host_cache)))
675            # Update the existing facts
676            host_cache.update(facts)
677
678        # Save the facts back to the backing store
679        self._fact_cache[host] = host_cache
680
681    def set_nonpersistent_facts(self, host, facts):
682        '''
683        Sets or updates the given facts for a host in the fact cache.
684        '''
685
686        if not isinstance(facts, Mapping):
687            raise AnsibleAssertionError("the type of 'facts' to set for nonpersistent_facts should be a Mapping but is a %s" % type(facts))
688
689        try:
690            self._nonpersistent_fact_cache[host].update(facts)
691        except KeyError:
692            self._nonpersistent_fact_cache[host] = facts
693
694    def set_host_variable(self, host, varname, value):
695        '''
696        Sets a value in the vars_cache for a host.
697        '''
698        if host not in self._vars_cache:
699            self._vars_cache[host] = dict()
700        if varname in self._vars_cache[host] and isinstance(self._vars_cache[host][varname], MutableMapping) and isinstance(value, MutableMapping):
701            self._vars_cache[host] = combine_vars(self._vars_cache[host], {varname: value})
702        else:
703            self._vars_cache[host][varname] = value
704
705
706class VarsWithSources(MutableMapping):
707    '''
708    Dict-like class for vars that also provides source information for each var
709
710    This class can only store the source for top-level vars. It does no tracking
711    on its own, just shows a debug message with the information that it is provided
712    when a particular var is accessed.
713    '''
714    def __init__(self, *args, **kwargs):
715        ''' Dict-compatible constructor '''
716        self.data = dict(*args, **kwargs)
717        self.sources = {}
718
719    @classmethod
720    def new_vars_with_sources(cls, data, sources):
721        ''' Alternate constructor method to instantiate class with sources '''
722        v = cls(data)
723        v.sources = sources
724        return v
725
726    def get_source(self, key):
727        return self.sources.get(key, None)
728
729    def __getitem__(self, key):
730        val = self.data[key]
731        # See notes in the VarsWithSources docstring for caveats and limitations of the source tracking
732        display.debug("variable '%s' from source: %s" % (key, self.sources.get(key, "unknown")))
733        return val
734
735    def __setitem__(self, key, value):
736        self.data[key] = value
737
738    def __delitem__(self, key):
739        del self.data[key]
740
741    def __iter__(self):
742        return iter(self.data)
743
744    def __len__(self):
745        return len(self.data)
746
747    # Prevent duplicate debug messages by defining our own __contains__ pointing at the underlying dict
748    def __contains__(self, key):
749        return self.data.__contains__(key)
750
751    def copy(self):
752        return VarsWithSources.new_vars_with_sources(self.data.copy(), self.sources.copy())
753