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                    temp_vars = combine_vars(all_vars, self._extra_vars)
332                    temp_vars = combine_vars(temp_vars, magic_variables)
333                    templar = Templar(loader=self._loader, variables=temp_vars)
334
335                    # we assume each item in the list is itself a list, as we
336                    # support "conditional includes" for vars_files, which mimics
337                    # the with_first_found mechanism.
338                    vars_file_list = vars_file_item
339                    if not isinstance(vars_file_list, list):
340                        vars_file_list = [vars_file_list]
341
342                    # now we iterate through the (potential) files, and break out
343                    # as soon as we read one from the list. If none are found, we
344                    # raise an error, which is silently ignored at this point.
345                    try:
346                        for vars_file in vars_file_list:
347                            vars_file = templar.template(vars_file)
348                            if not (isinstance(vars_file, Sequence)):
349                                raise AnsibleError(
350                                    "Invalid vars_files entry found: %r\n"
351                                    "vars_files entries should be either a string type or "
352                                    "a list of string types after template expansion" % vars_file
353                                )
354                            try:
355                                data = preprocess_vars(self._loader.load_from_file(vars_file, unsafe=True))
356                                if data is not None:
357                                    for item in data:
358                                        all_vars = _combine_and_track(all_vars, item, "play vars_files from '%s'" % vars_file)
359                                break
360                            except AnsibleFileNotFound:
361                                # we continue on loader failures
362                                continue
363                            except AnsibleParserError:
364                                raise
365                        else:
366                            # if include_delegate_to is set to False, we ignore the missing
367                            # vars file here because we're working on a delegated host
368                            if include_delegate_to:
369                                raise AnsibleFileNotFound("vars file %s was not found" % vars_file_item)
370                    except (UndefinedError, AnsibleUndefinedVariable):
371                        if host is not None and self._fact_cache.get(host.name, dict()).get('module_setup') and task is not None:
372                            raise AnsibleUndefinedVariable("an undefined variable was found when attempting to template the vars_files item '%s'"
373                                                           % vars_file_item, obj=vars_file_item)
374                        else:
375                            # we do not have a full context here, and the missing variable could be because of that
376                            # so just show a warning and continue
377                            display.vvv("skipping vars_file '%s' due to an undefined variable" % vars_file_item)
378                            continue
379
380                    display.vvv("Read vars_file '%s'" % vars_file_item)
381            except TypeError:
382                raise AnsibleParserError("Error while reading vars files - please supply a list of file names. "
383                                         "Got '%s' of type %s" % (vars_files, type(vars_files)))
384
385            # By default, we now merge in all vars from all roles in the play,
386            # unless the user has disabled this via a config option
387            if not C.DEFAULT_PRIVATE_ROLE_VARS:
388                for role in play.get_roles():
389                    all_vars = _combine_and_track(all_vars, role.get_vars(include_params=False), "role '%s' vars" % role.name)
390
391        # next, we merge in the vars from the role, which will specifically
392        # follow the role dependency chain, and then we merge in the tasks
393        # vars (which will look at parent blocks/task includes)
394        if task:
395            if task._role:
396                all_vars = _combine_and_track(all_vars, task._role.get_vars(task.get_dep_chain(), include_params=False),
397                                              "role '%s' vars" % task._role.name)
398            all_vars = _combine_and_track(all_vars, task.get_vars(), "task vars")
399
400        # next, we merge in the vars cache (include vars) and nonpersistent
401        # facts cache (set_fact/register), in that order
402        if host:
403            # include_vars non-persistent cache
404            all_vars = _combine_and_track(all_vars, self._vars_cache.get(host.get_name(), dict()), "include_vars")
405            # fact non-persistent cache
406            all_vars = _combine_and_track(all_vars, self._nonpersistent_fact_cache.get(host.name, dict()), "set_fact")
407
408        # next, we merge in role params and task include params
409        if task:
410            if task._role:
411                all_vars = _combine_and_track(all_vars, task._role.get_role_params(task.get_dep_chain()), "role '%s' params" % task._role.name)
412
413            # special case for include tasks, where the include params
414            # may be specified in the vars field for the task, which should
415            # have higher precedence than the vars/np facts above
416            all_vars = _combine_and_track(all_vars, task.get_include_params(), "include params")
417
418        # extra vars
419        all_vars = _combine_and_track(all_vars, self._extra_vars, "extra vars")
420
421        # magic variables
422        all_vars = _combine_and_track(all_vars, magic_variables, "magic vars")
423
424        # special case for the 'environment' magic variable, as someone
425        # may have set it as a variable and we don't want to stomp on it
426        if task:
427            all_vars['environment'] = task.environment
428
429        # if we have a task and we're delegating to another host, figure out the
430        # variables for that host now so we don't have to rely on hostvars later
431        if task and task.delegate_to is not None and include_delegate_to:
432            all_vars['ansible_delegated_vars'], all_vars['_ansible_loop_cache'] = self._get_delegated_vars(play, task, all_vars)
433
434        # 'vars' magic var
435        if task or play:
436            # has to be copy, otherwise recursive ref
437            all_vars['vars'] = all_vars.copy()
438
439        display.debug("done with get_vars()")
440        if C.DEFAULT_DEBUG:
441            # Use VarsWithSources wrapper class to display var sources
442            return VarsWithSources.new_vars_with_sources(all_vars, _vars_sources)
443        else:
444            return all_vars
445
446    def _get_magic_variables(self, play, host, task, include_hostvars, include_delegate_to,
447                             _hosts=None, _hosts_all=None):
448        '''
449        Returns a dictionary of so-called "magic" variables in Ansible,
450        which are special variables we set internally for use.
451        '''
452
453        variables = {}
454        variables['playbook_dir'] = os.path.abspath(self._loader.get_basedir())
455        variables['ansible_playbook_python'] = sys.executable
456        variables['ansible_config_file'] = C.CONFIG_FILE
457
458        if play:
459            # This is a list of all role names of all dependencies for all roles for this play
460            dependency_role_names = list(set([d.get_name() for r in play.roles for d in r.get_all_dependencies()]))
461            # This is a list of all role names of all roles for this play
462            play_role_names = [r.get_name() for r in play.roles]
463
464            # ansible_role_names includes all role names, dependent or directly referenced by the play
465            variables['ansible_role_names'] = list(set(dependency_role_names + play_role_names))
466            # ansible_play_role_names includes the names of all roles directly referenced by this play
467            # roles that are implicitly referenced via dependencies are not listed.
468            variables['ansible_play_role_names'] = play_role_names
469            # ansible_dependent_role_names includes the names of all roles that are referenced via dependencies
470            # dependencies that are also explicitly named as roles are included in this list
471            variables['ansible_dependent_role_names'] = dependency_role_names
472
473            # DEPRECATED: role_names should be deprecated in favor of ansible_role_names or ansible_play_role_names
474            variables['role_names'] = variables['ansible_play_role_names']
475
476            variables['ansible_play_name'] = play.get_name()
477
478        if task:
479            if task._role:
480                variables['role_name'] = task._role.get_name(include_role_fqcn=False)
481                variables['role_path'] = task._role._role_path
482                variables['role_uuid'] = text_type(task._role._uuid)
483                variables['ansible_collection_name'] = task._role._role_collection
484                variables['ansible_role_name'] = task._role.get_name()
485
486        if self._inventory is not None:
487            variables['groups'] = self._inventory.get_groups_dict()
488            if play:
489                templar = Templar(loader=self._loader)
490                if templar.is_template(play.hosts):
491                    pattern = 'all'
492                else:
493                    pattern = play.hosts or 'all'
494                # add the list of hosts in the play, as adjusted for limit/filters
495                if not _hosts_all:
496                    _hosts_all = [h.name for h in self._inventory.get_hosts(pattern=pattern, ignore_restrictions=True)]
497                if not _hosts:
498                    _hosts = [h.name for h in self._inventory.get_hosts()]
499
500                variables['ansible_play_hosts_all'] = _hosts_all[:]
501                variables['ansible_play_hosts'] = [x for x in variables['ansible_play_hosts_all'] if x not in play._removed_hosts]
502                variables['ansible_play_batch'] = [x for x in _hosts if x not in play._removed_hosts]
503
504                # DEPRECATED: play_hosts should be deprecated in favor of ansible_play_batch,
505                # however this would take work in the templating engine, so for now we'll add both
506                variables['play_hosts'] = variables['ansible_play_batch']
507
508        # the 'omit' value allows params to be left out if the variable they are based on is undefined
509        variables['omit'] = self._omit_token
510        # Set options vars
511        for option, option_value in iteritems(self._options_vars):
512            variables[option] = option_value
513
514        if self._hostvars is not None and include_hostvars:
515            variables['hostvars'] = self._hostvars
516
517        return variables
518
519    def _get_delegated_vars(self, play, task, existing_variables):
520        if not hasattr(task, 'loop'):
521            # This "task" is not a Task, so we need to skip it
522            return {}, None
523
524        # we unfortunately need to template the delegate_to field here,
525        # as we're fetching vars before post_validate has been called on
526        # the task that has been passed in
527        vars_copy = existing_variables.copy()
528        templar = Templar(loader=self._loader, variables=vars_copy)
529
530        items = []
531        has_loop = True
532        if task.loop_with is not None:
533            if task.loop_with in lookup_loader:
534                try:
535                    loop_terms = listify_lookup_plugin_terms(terms=task.loop, templar=templar,
536                                                             loader=self._loader, fail_on_undefined=True, convert_bare=False)
537                    items = wrap_var(lookup_loader.get(task.loop_with, loader=self._loader, templar=templar).run(terms=loop_terms, variables=vars_copy))
538                except AnsibleTemplateError:
539                    # This task will be skipped later due to this, so we just setup
540                    # a dummy array for the later code so it doesn't fail
541                    items = [None]
542            else:
543                raise AnsibleError("Failed to find the lookup named '%s' in the available lookup plugins" % task.loop_with)
544        elif task.loop is not None:
545            try:
546                items = templar.template(task.loop)
547            except AnsibleTemplateError:
548                # This task will be skipped later due to this, so we just setup
549                # a dummy array for the later code so it doesn't fail
550                items = [None]
551        else:
552            has_loop = False
553            items = [None]
554
555        # since host can change per loop, we keep dict per host name resolved
556        delegated_host_vars = dict()
557        item_var = getattr(task.loop_control, 'loop_var', 'item')
558        cache_items = False
559        for item in items:
560            # update the variables with the item value for templating, in case we need it
561            if item is not None:
562                vars_copy[item_var] = item
563
564            templar.available_variables = vars_copy
565            delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False)
566            if delegated_host_name != task.delegate_to:
567                cache_items = True
568            if delegated_host_name is None:
569                raise AnsibleError(message="Undefined delegate_to host for task:", obj=task._ds)
570            if not isinstance(delegated_host_name, string_types):
571                raise AnsibleError(message="the field 'delegate_to' has an invalid type (%s), and could not be"
572                                           " converted to a string type." % type(delegated_host_name), obj=task._ds)
573
574            if delegated_host_name in delegated_host_vars:
575                # no need to repeat ourselves, as the delegate_to value
576                # does not appear to be tied to the loop item variable
577                continue
578
579            # now try to find the delegated-to host in inventory, or failing that,
580            # create a new host on the fly so we can fetch variables for it
581            delegated_host = None
582            if self._inventory is not None:
583                delegated_host = self._inventory.get_host(delegated_host_name)
584                # try looking it up based on the address field, and finally
585                # fall back to creating a host on the fly to use for the var lookup
586                if delegated_host is None:
587                    for h in self._inventory.get_hosts(ignore_limits=True, ignore_restrictions=True):
588                        # check if the address matches, or if both the delegated_to host
589                        # and the current host are in the list of localhost aliases
590                        if h.address == delegated_host_name:
591                            delegated_host = h
592                            break
593                    else:
594                        delegated_host = Host(name=delegated_host_name)
595            else:
596                delegated_host = Host(name=delegated_host_name)
597
598            # now we go fetch the vars for the delegated-to host and save them in our
599            # master dictionary of variables to be used later in the TaskExecutor/PlayContext
600            delegated_host_vars[delegated_host_name] = self.get_vars(
601                play=play,
602                host=delegated_host,
603                task=task,
604                include_delegate_to=False,
605                include_hostvars=True,
606            )
607            delegated_host_vars[delegated_host_name]['inventory_hostname'] = vars_copy.get('inventory_hostname')
608
609        _ansible_loop_cache = None
610        if has_loop and cache_items:
611            # delegate_to templating produced a change, so we will cache the templated items
612            # in a special private hostvar
613            # this ensures that delegate_to+loop doesn't produce different results than TaskExecutor
614            # which may reprocess the loop
615            _ansible_loop_cache = items
616
617        return delegated_host_vars, _ansible_loop_cache
618
619    def clear_facts(self, hostname):
620        '''
621        Clears the facts for a host
622        '''
623        self._fact_cache.pop(hostname, None)
624
625    def set_host_facts(self, host, facts):
626        '''
627        Sets or updates the given facts for a host in the fact cache.
628        '''
629
630        if not isinstance(facts, Mapping):
631            raise AnsibleAssertionError("the type of 'facts' to set for host_facts should be a Mapping but is a %s" % type(facts))
632
633        try:
634            host_cache = self._fact_cache[host]
635        except KeyError:
636            # We get to set this as new
637            host_cache = facts
638        else:
639            if not isinstance(host_cache, MutableMapping):
640                raise TypeError('The object retrieved for {0} must be a MutableMapping but was'
641                                ' a {1}'.format(host, type(host_cache)))
642            # Update the existing facts
643            host_cache.update(facts)
644
645        # Save the facts back to the backing store
646        self._fact_cache[host] = host_cache
647
648    def set_nonpersistent_facts(self, host, facts):
649        '''
650        Sets or updates the given facts for a host in the fact cache.
651        '''
652
653        if not isinstance(facts, Mapping):
654            raise AnsibleAssertionError("the type of 'facts' to set for nonpersistent_facts should be a Mapping but is a %s" % type(facts))
655
656        try:
657            self._nonpersistent_fact_cache[host].update(facts)
658        except KeyError:
659            self._nonpersistent_fact_cache[host] = facts
660
661    def set_host_variable(self, host, varname, value):
662        '''
663        Sets a value in the vars_cache for a host.
664        '''
665        if host not in self._vars_cache:
666            self._vars_cache[host] = dict()
667        if varname in self._vars_cache[host] and isinstance(self._vars_cache[host][varname], MutableMapping) and isinstance(value, MutableMapping):
668            self._vars_cache[host] = combine_vars(self._vars_cache[host], {varname: value})
669        else:
670            self._vars_cache[host][varname] = value
671
672
673class VarsWithSources(MutableMapping):
674    '''
675    Dict-like class for vars that also provides source information for each var
676
677    This class can only store the source for top-level vars. It does no tracking
678    on its own, just shows a debug message with the information that it is provided
679    when a particular var is accessed.
680    '''
681    def __init__(self, *args, **kwargs):
682        ''' Dict-compatible constructor '''
683        self.data = dict(*args, **kwargs)
684        self.sources = {}
685
686    @classmethod
687    def new_vars_with_sources(cls, data, sources):
688        ''' Alternate constructor method to instantiate class with sources '''
689        v = cls(data)
690        v.sources = sources
691        return v
692
693    def get_source(self, key):
694        return self.sources.get(key, None)
695
696    def __getitem__(self, key):
697        val = self.data[key]
698        # See notes in the VarsWithSources docstring for caveats and limitations of the source tracking
699        display.debug("variable '%s' from source: %s" % (key, self.sources.get(key, "unknown")))
700        return val
701
702    def __setitem__(self, key, value):
703        self.data[key] = value
704
705    def __delitem__(self, key):
706        del self.data[key]
707
708    def __iter__(self):
709        return iter(self.data)
710
711    def __len__(self):
712        return len(self.data)
713
714    # Prevent duplicate debug messages by defining our own __contains__ pointing at the underlying dict
715    def __contains__(self, key):
716        return self.data.__contains__(key)
717
718    def copy(self):
719        return VarsWithSources.new_vars_with_sources(self.data.copy(), self.sources.copy())
720