From d91b3c6b9d767005e88c0e991de2a15692bca612 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Mon, 5 Oct 2015 16:31:08 -0400 Subject: [PATCH] Make sure magic variables are available when templating vars_files Also does some reorganization/cleanup on the magic vars/delegated variable generation portions of VariableManager to make the above possible. Fixes #12633 --- lib/ansible/executor/task_executor.py | 2 +- lib/ansible/vars/__init__.py | 219 ++++++++++++++------------ 2 files changed, 119 insertions(+), 102 deletions(-) diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py index f13029d82cf..99e12acb149 100644 --- a/lib/ansible/executor/task_executor.py +++ b/lib/ansible/executor/task_executor.py @@ -486,7 +486,7 @@ class TaskExecutor: del variables[i] # now replace the interpreter values with those that may have come # from the delegated-to host - delegated_vars = variables.get('ansible_delegated_vars', dict()) + delegated_vars = variables.get('ansible_delegated_vars', dict()).get(self._host.name, dict()) if isinstance(delegated_vars, dict): for i in delegated_vars: if i.startswith("ansible_") and i.endswith("_interpreter"): diff --git a/lib/ansible/vars/__init__.py b/lib/ansible/vars/__init__.py index e313a2bcf18..ec2ed992228 100644 --- a/lib/ansible/vars/__init__.py +++ b/lib/ansible/vars/__init__.py @@ -188,6 +188,14 @@ class VariableManager: return VARIABLE_CACHE[cache_entry] all_vars = defaultdict(dict) + magic_variables = self._get_magic_variables( + loader=loader, + play=play, + host=host, + task=task, + include_hostvars=include_hostvars, + include_delegate_to=include_delegate_to, + ) if play: # first we compile any vars specified in defaults/main.yml @@ -247,9 +255,10 @@ class VariableManager: all_vars = combine_vars(all_vars, play.get_vars()) for vars_file_item in play.get_vars_files(): - # create a set of temporary vars here, which incorporate the - # extra vars so we can properly template the vars_files entries + # create a set of temporary vars here, which incorporate the extra + # and magic vars so we can properly template the vars_files entries temp_vars = combine_vars(all_vars, self._extra_vars) + temp_vars = combine_vars(all_vars, magic_variables) templar = Templar(loader=loader, variables=temp_vars) # we assume each item in the list is itself a list, as we @@ -302,19 +311,34 @@ class VariableManager: all_vars = combine_vars(all_vars, self._nonpersistent_fact_cache.get(host.name, dict())) all_vars = combine_vars(all_vars, self._extra_vars) + all_vars = combine_vars(all_vars, magic_variables) - # FIXME: make sure all special vars are here - # Finally, we create special vars + # if we have a task and we're delegating to another host, figure out the + # variables for that host now so we don't have to rely on hostvars later + if task and task.delegate_to is not None and include_delegate_to: + all_vars['ansible_delegated_vars'] = self._get_delegated_vars(loader, play, task, all_vars) - all_vars['playbook_dir'] = loader.get_basedir() + #VARIABLE_CACHE[cache_entry] = all_vars + + debug("done with get_vars()") + return all_vars + + def _get_magic_variables(self, loader, play, host, task, include_hostvars, include_delegate_to): + ''' + Returns a dictionary of so-called "magic" variables in Ansible, + which are special variables we set internally for use. + ''' + + variables = dict() + variables['playbook_dir'] = loader.get_basedir() if host: - all_vars['group_names'] = [group.name for group in host.get_groups()] + variables['group_names'] = [group.name for group in host.get_groups()] if self._inventory is not None: - all_vars['groups'] = dict() + variables['groups'] = dict() for (group_name, group) in iteritems(self._inventory.groups): - all_vars['groups'][group_name] = [h.name for h in group.get_hosts()] + variables['groups'][group_name] = [h.name for h in group.get_hosts()] if include_hostvars: hostvars_cache_entry = self._get_cache_entry(play=play) @@ -323,115 +347,108 @@ class VariableManager: else: hostvars = HostVars(play=play, inventory=self._inventory, loader=loader, variable_manager=self) HOSTVARS_CACHE[hostvars_cache_entry] = hostvars - all_vars['hostvars'] = hostvars + variables['hostvars'] = hostvars + variables['vars'] = hostvars[host.get_name()] if task: if task._role: - all_vars['role_path'] = task._role._role_path - - # if we have a task and we're delegating to another host, figure out the - # variables for that host now so we don't have to rely on hostvars later - if task.delegate_to is not None and include_delegate_to: - # we unfortunately need to template the delegate_to field here, - # as we're fetching vars before post_validate has been called on - # the task that has been passed in - templar = Templar(loader=loader, variables=all_vars) - - items = [] - if task.loop is not None: - if task.loop in lookup_loader: - #TODO: remove convert_bare true and deprecate this in with_ - try: - loop_terms = listify_lookup_plugin_terms(terms=task.loop_args, templar=templar, loader=loader, fail_on_undefined=True, convert_bare=True) - except AnsibleUndefinedVariable as e: - if 'has no attribute' in str(e): - loop_terms = [] - self._display.deprecated("Skipping task due to undefined attribute, in the future this will be a fatal error.") - else: - raise - items = lookup_loader.get(task.loop, loader=loader, templar=templar).run(terms=loop_terms, variables=all_vars) - else: - raise AnsibleError("Unexpected failure in finding the lookup named '%s' in the available lookup plugins" % task.loop) - else: - items = [None] - - vars_copy = all_vars.copy() - delegated_host_vars = dict() - for item in items: - # update the variables with the item value for templating, in case we need it - if item is not None: - vars_copy['item'] = item - - templar.set_available_variables(vars_copy) - delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False) - if delegated_host_name in delegated_host_vars: - # no need to repeat ourselves, as the delegate_to value - # does not appear to be tied to the loop item variable - continue - - # a dictionary of variables to use if we have to create a new host below - new_delegated_host_vars = dict( - ansible_host=delegated_host_name, - ansible_user=C.DEFAULT_REMOTE_USER, - ansible_connection=C.DEFAULT_TRANSPORT, - ) - - # now try to find the delegated-to host in inventory, or failing that, - # create a new host on the fly so we can fetch variables for it - delegated_host = None - if self._inventory is not None: - delegated_host = self._inventory.get_host(delegated_host_name) - # try looking it up based on the address field, and finally - # fall back to creating a host on the fly to use for the var lookup - if delegated_host is None: - for h in self._inventory.get_hosts(ignore_limits_and_restrictions=True): - # check if the address matches, or if both the delegated_to host - # and the current host are in the list of localhost aliases - if h.address == delegated_host_name or h.name in C.LOCALHOST and delegated_host_name in C.LOCALHOST: - delegated_host = h - break - else: - delegated_host = Host(name=delegated_host_name) - delegated_host.vars.update(new_delegated_host_vars) - else: - delegated_host = Host(name=delegated_host_name) - delegated_host.vars.update(new_delegated_host_vars) - - # now we go fetch the vars for the delegated-to host and save them in our - # master dictionary of variables to be used later in the TaskExecutor/PlayContext - delegated_host_vars[delegated_host_name] = self.get_vars( - loader=loader, - play=play, - host=delegated_host, - task=task, - include_delegate_to=False, - include_hostvars=False, - ) - - all_vars['ansible_delegated_vars'] = delegated_host_vars + variables['role_path'] = task._role._role_path if self._inventory is not None: - all_vars['inventory_dir'] = self._inventory.basedir() + variables['inventory_dir'] = self._inventory.basedir() if play: # add the list of hosts in the play, as adjusted for limit/filters # DEPRECATED: play_hosts should be deprecated in favor of ansible_play_hosts, # however this would take work in the templating engine, so for now # we'll add both so we can give users something transitional to use host_list = [x.name for x in self._inventory.get_hosts()] - all_vars['play_hosts'] = host_list - all_vars['ansible_play_hosts'] = host_list + variables['play_hosts'] = host_list + variables['ansible_play_hosts'] = host_list # the 'omit' value alows params to be left out if the variable they are based on is undefined - all_vars['omit'] = self._omit_token - all_vars['ansible_version'] = CLI.version_info(gitinfo=False) + variables['omit'] = self._omit_token + variables['ansible_version'] = CLI.version_info(gitinfo=False) - if 'hostvars' in all_vars and host: - all_vars['vars'] = all_vars['hostvars'][host.get_name()] + return variables - #VARIABLE_CACHE[cache_entry] = all_vars + def _get_delegated_vars(self, loader, play, task, existing_variables): + # we unfortunately need to template the delegate_to field here, + # as we're fetching vars before post_validate has been called on + # the task that has been passed in + vars_copy = existing_variables.copy() + templar = Templar(loader=loader, variables=vars_copy) - debug("done with get_vars()") - return all_vars + items = [] + if task.loop is not None: + if task.loop in lookup_loader: + #TODO: remove convert_bare true and deprecate this in with_ + try: + loop_terms = listify_lookup_plugin_terms(terms=task.loop_args, templar=templar, loader=loader, fail_on_undefined=True, convert_bare=True) + except AnsibleUndefinedVariable as e: + if 'has no attribute' in str(e): + loop_terms = [] + self._display.deprecated("Skipping task due to undefined attribute, in the future this will be a fatal error.") + else: + raise + items = lookup_loader.get(task.loop, loader=loader, templar=templar).run(terms=loop_terms, variables=vars_copy) + else: + raise AnsibleError("Unexpected failure in finding the lookup named '%s' in the available lookup plugins" % task.loop) + else: + items = [None] + + delegated_host_vars = dict() + for item in items: + # update the variables with the item value for templating, in case we need it + if item is not None: + vars_copy['item'] = item + + templar.set_available_variables(vars_copy) + delegated_host_name = templar.template(task.delegate_to, fail_on_undefined=False) + if delegated_host_name in delegated_host_vars: + # no need to repeat ourselves, as the delegate_to value + # does not appear to be tied to the loop item variable + continue + + # a dictionary of variables to use if we have to create a new host below + new_delegated_host_vars = dict( + ansible_host=delegated_host_name, + ansible_user=C.DEFAULT_REMOTE_USER, + ansible_connection=C.DEFAULT_TRANSPORT, + ) + + # now try to find the delegated-to host in inventory, or failing that, + # create a new host on the fly so we can fetch variables for it + delegated_host = None + if self._inventory is not None: + delegated_host = self._inventory.get_host(delegated_host_name) + # try looking it up based on the address field, and finally + # fall back to creating a host on the fly to use for the var lookup + if delegated_host is None: + for h in self._inventory.get_hosts(ignore_limits_and_restrictions=True): + # check if the address matches, or if both the delegated_to host + # and the current host are in the list of localhost aliases + if h.address == delegated_host_name or h.name in C.LOCALHOST and delegated_host_name in C.LOCALHOST: + delegated_host = h + break + else: + delegated_host = Host(name=delegated_host_name) + delegated_host.vars.update(new_delegated_host_vars) + else: + delegated_host = Host(name=delegated_host_name) + delegated_host.vars.update(new_delegated_host_vars) + + # now we go fetch the vars for the delegated-to host and save them in our + # master dictionary of variables to be used later in the TaskExecutor/PlayContext + delegated_host_vars[delegated_host_name] = self.get_vars( + loader=loader, + play=play, + host=delegated_host, + task=task, + include_delegate_to=False, + include_hostvars=False, + ) + + return delegated_host_vars def _get_inventory_basename(self, path): '''