From 143904f49b806322b1ae95b5b53057f644bf9665 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 23 Feb 2022 15:16:03 -0500 Subject: [PATCH] More informative playbook attribute errors (#77082) * More informative playbook attribute errors Co-authored-by: Sloane Hertel <19572925+s-hertel@users.noreply.github.com> --- .../more_targeted_attribute_errors.yml | 2 ++ lib/ansible/playbook/base.py | 24 ++++++++++--------- lib/ansible/plugins/strategy/__init__.py | 15 ++++++++---- 3 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/more_targeted_attribute_errors.yml diff --git a/changelogs/fragments/more_targeted_attribute_errors.yml b/changelogs/fragments/more_targeted_attribute_errors.yml new file mode 100644 index 00000000000..6c73da3543d --- /dev/null +++ b/changelogs/fragments/more_targeted_attribute_errors.yml @@ -0,0 +1,2 @@ +bugfixes: + - playbook/strategy have more informative 'attribute' based errors for playbook objects and handlers. diff --git a/lib/ansible/playbook/base.py b/lib/ansible/playbook/base.py index 55a1330674c..97bd7297fd6 100644 --- a/lib/ansible/playbook/base.py +++ b/lib/ansible/playbook/base.py @@ -16,10 +16,9 @@ from jinja2.exceptions import UndefinedError from ansible import constants as C from ansible import context -from ansible.errors import AnsibleError +from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError from ansible.module_utils.six import string_types from ansible.module_utils.parsing.convert_bool import boolean -from ansible.errors import AnsibleParserError, AnsibleUndefinedVariable, AnsibleAssertionError from ansible.module_utils._text import to_text, to_native from ansible.parsing.dataloader import DataLoader from ansible.playbook.attribute import Attribute, FieldAttribute @@ -36,7 +35,7 @@ def _generic_g(prop_name, self): try: value = self._attributes[prop_name] except KeyError: - raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + raise AttributeError("'%s' does not have the keyword '%s'" % (self.__class__.__name__, prop_name)) if value is Sentinel: value = self._attr_defaults[prop_name] @@ -51,7 +50,7 @@ def _generic_g_method(prop_name, self): method = "_get_attr_%s" % prop_name return getattr(self, method)() except KeyError: - raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + raise AttributeError("'%s' does not support the keyword '%s'" % (self.__class__.__name__, prop_name)) def _generic_g_parent(prop_name, self): @@ -64,7 +63,7 @@ def _generic_g_parent(prop_name, self): except AttributeError: value = self._attributes[prop_name] except KeyError: - raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, prop_name)) + raise AttributeError("'%s' nor it's parents support the keyword '%s'" % (self.__class__.__name__, prop_name)) if value is Sentinel: value = self._attr_defaults[prop_name] @@ -146,12 +145,15 @@ class BaseMeta(type): # method, or if the attribute is marked as not inheriting # its value from a parent object method = "_get_attr_%s" % attr_name - if method in src_dict or method in dst_dict: - getter = partial(_generic_g_method, attr_name) - elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit: - getter = partial(_generic_g_parent, attr_name) - else: - getter = partial(_generic_g, attr_name) + try: + if method in src_dict or method in dst_dict: + getter = partial(_generic_g_method, attr_name) + elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit: + getter = partial(_generic_g_parent, attr_name) + else: + getter = partial(_generic_g, attr_name) + except AttributeError as e: + raise AnsibleParserError("Invalid playbook definition: %s" % to_native(e), orig_exc=e) setter = partial(_generic_s, attr_name) deleter = partial(_generic_d, attr_name) diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py index 817c86f4b7f..9c9846a997f 100644 --- a/lib/ansible/plugins/strategy/__init__.py +++ b/lib/ansible/plugins/strategy/__init__.py @@ -26,6 +26,7 @@ import pprint import sys import threading import time +import traceback from collections import deque from multiprocessing import Lock @@ -42,7 +43,7 @@ from ansible.executor.process.worker import WorkerProcess from ansible.executor.task_result import TaskResult from ansible.executor.task_queue_manager import CallbackSend from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_text +from ansible.module_utils._text import to_text, to_native from ansible.module_utils.connection import Connection, ConnectionError from ansible.playbook.conditional import Conditional from ansible.playbook.handler import Handler @@ -1037,10 +1038,14 @@ class StrategyBase: # but this may take some work in the iterator and gets tricky when # we consider the ability of meta tasks to flush handlers for handler in handler_block.block: - if handler.notified_hosts: - result = self._do_handler_run(handler, handler.get_name(), iterator=iterator, play_context=play_context) - if not result: - break + try: + if handler.notified_hosts: + result = self._do_handler_run(handler, handler.get_name(), iterator=iterator, play_context=play_context) + if not result: + break + except AttributeError as e: + display.vvv(traceback.format_exc()) + raise AnsibleParserError("Invalid handler definition for '%s'" % (handler.get_name()), orig_exc=e) return result def _do_handler_run(self, handler, handler_name, iterator, play_context, notified_hosts=None):