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