diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py index e0c426df3be..1a1cc4f9762 100644 --- a/lib/ansible/playbook/conditional.py +++ b/lib/ansible/playbook/conditional.py @@ -19,6 +19,8 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import re + from jinja2.exceptions import UndefinedError from ansible.compat.six import text_type @@ -27,6 +29,8 @@ from ansible.playbook.attribute import FieldAttribute from ansible.template import Templar from ansible.module_utils._text import to_native +DEFINED_REGEX = re.compile(r'(hostvars\[.+\]|[\w_]+)\s+(not\s+is|is|is\s+not)\s+(defined|undefined)') + class Conditional: ''' @@ -62,6 +66,18 @@ class Conditional: when = self._get_parent_attribute('when', extend=True, prepend=True) return when + def extract_defined_undefined(self, conditional): + results = [] + + cond = conditional + m = DEFINED_REGEX.search(cond) + while m: + results.append(m.groups()) + cond = cond[m.end():] + m = DEFINED_REGEX.search(cond) + + return results + def evaluate_conditional(self, templar, all_vars): ''' Loops through the conditionals set on this object, returning @@ -121,14 +137,31 @@ class Conditional: else: raise AnsibleError("unable to evaluate conditional: %s" % original) except (AnsibleUndefinedVariable, UndefinedError) as e: - # the templating failed, meaning most likely a - # variable was undefined. If we happened to be - # looking for an undefined variable, return True, - # otherwise fail - if "is undefined" in original or "is not defined" in original or "not is defined" in original: - return True - elif "is defined" in original or "is not undefined" in original or "not is undefined" in original: - return False - else: + # the templating failed, meaning most likely a variable was undefined. If we happened to be + # looking for an undefined variable, return True, otherwise fail + try: + # first we extract the variable name from the error message + var_name = re.compile(r"'(hostvars\[.+\]|[\w_]+)' is undefined").search(str(e)).groups()[0] + # next we extract all defined/undefined tests from the conditional string + def_undef = self.extract_defined_undefined(conditional) + # then we loop through these, comparing the error variable name against + # each def/undef test we found above. If there is a match, we determine + # whether the logic/state mean the variable should exist or not and return + # the corresponding True/False + for (du_var, logic, state) in def_undef: + # when we compare the var names, normalize quotes because something + # like hostvars['foo'] may be tested against hostvars["foo"] + if var_name.replace("'", '"') == du_var.replace("'", '"'): + # the should exist is a xor test between a negation in the logic portion + # against the state (defined or undefined) + should_exist = ('not' in logic) != (state == 'defined') + if should_exist: + return False + else: + return True + # as nothing above matched the failed var name, re-raise here to + # trigger the AnsibleUndefinedVariable exception again below + raise + except Exception as new_e: raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e)) diff --git a/lib/ansible/vars/hostvars.py b/lib/ansible/vars/hostvars.py index 74262b2ed46..3922b827a7b 100644 --- a/lib/ansible/vars/hostvars.py +++ b/lib/ansible/vars/hostvars.py @@ -73,7 +73,7 @@ class HostVars(collections.Mapping): ''' host = self._find_host(host_name) if host is None: - raise UndefinedError("%s not found in hostvars" % host_name) + raise UndefinedError("'hostvars[\"%s\"]' is undefined" % host_name) return self._variable_manager.get_vars(loader=self._loader, host=host, include_hostvars=False)