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