Use underlying `AnsibleError` exception as cause

This has the same effect as doing

    raise AnsibleError from UnderlyingError

The exception cause is then available for inspection and shows up
in tracebacks.
pull/82730/head
Sviatoslav Sydorenko 3 months ago
parent a1edb61ce7
commit 096c69523c
No known key found for this signature in database
GPG Key ID: 9345E8FEA89CA455

@ -21,6 +21,7 @@ import re
import traceback import traceback
from collections.abc import Sequence from collections.abc import Sequence
from warnings import warn as _emit_warning
from ansible.errors.yaml_strings import ( from ansible.errors.yaml_strings import (
YAML_COMMON_DICT_ERROR, YAML_COMMON_DICT_ERROR,
@ -35,6 +36,10 @@ from ansible.errors.yaml_strings import (
from ansible.module_utils.common.text.converters import to_native, to_text from ansible.module_utils.common.text.converters import to_native, to_text
_UNDERLYING_EXC_SENTINEL = Exception()
"""A sentinel object for use as a default for arguments in callables."""
class AnsibleError(Exception): class AnsibleError(Exception):
''' '''
This is the base class for all errors raised from Ansible code, This is the base class for all errors raised from Ansible code,
@ -50,14 +55,42 @@ class AnsibleError(Exception):
which should be returned by the DataLoader() class. which should be returned by the DataLoader() class.
''' '''
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None): def __init__(
self,
message="",
obj=None,
show_content=True,
suppress_extended_error=False,
orig_exc=_UNDERLYING_EXC_SENTINEL,
):
super(AnsibleError, self).__init__(message) super(AnsibleError, self).__init__(message)
self._show_content = show_content self._show_content = show_content
self._suppress_extended_error = suppress_extended_error self._suppress_extended_error = suppress_extended_error
self._message = to_native(message) self._message = to_native(message)
self.obj = obj self.obj = obj
self.orig_exc = orig_exc if orig_exc is not _UNDERLYING_EXC_SENTINEL:
_emit_warning(
'Passing the underlying error through the `orig_exc` argument '
'is deprecated. Instead, use native Python 3 exception cause '
'chaining syntax: `raise ... from ...`',
DeprecationWarning,
stacklevel=2,
)
self.__cause__ = orig_exc
@property
def orig_exc(self):
"""The underlying exception object.
If the exception cause is undefined, it's set to :py:data:`None`.
"""
return self.__cause__
@orig_exc.setter
def orig_exc(self, underlying_exception):
"""Set the underlying exception as cause of the current one."""
self.__cause__ = underlying_exception
@property @property
def message(self): def message(self):
@ -72,8 +105,8 @@ class AnsibleError(Exception):
message.append( message.append(
'\n\n%s' % to_native(extended_error) '\n\n%s' % to_native(extended_error)
) )
elif self.orig_exc: elif self.__cause__:
message.append('. %s' % to_native(self.orig_exc)) message.append('. %s' % to_native(self.__cause__))
return ''.join(message) return ''.join(message)
@ -285,7 +318,16 @@ class AnsibleUndefinedVariable(AnsibleTemplateError):
class AnsibleFileNotFound(AnsibleRuntimeError): class AnsibleFileNotFound(AnsibleRuntimeError):
''' a file missing failure ''' ''' a file missing failure '''
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, paths=None, file_name=None): def __init__(
self,
message="",
obj=None,
show_content=True,
suppress_extended_error=False,
orig_exc=_UNDERLYING_EXC_SENTINEL,
paths=None,
file_name=None,
):
self.file_name = file_name self.file_name = file_name
self.paths = paths self.paths = paths
@ -315,7 +357,15 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
class AnsibleAction(AnsibleRuntimeError): class AnsibleAction(AnsibleRuntimeError):
''' Base Exception for Action plugin flow control ''' ''' Base Exception for Action plugin flow control '''
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): def __init__(
self,
message="",
obj=None,
show_content=True,
suppress_extended_error=False,
orig_exc=_UNDERLYING_EXC_SENTINEL,
result=None,
):
super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content, super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc) suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
@ -328,7 +378,15 @@ class AnsibleAction(AnsibleRuntimeError):
class AnsibleActionSkip(AnsibleAction): class AnsibleActionSkip(AnsibleAction):
''' an action runtime skip''' ''' an action runtime skip'''
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): def __init__(
self,
message="",
obj=None,
show_content=True,
suppress_extended_error=False,
orig_exc=_UNDERLYING_EXC_SENTINEL,
result=None,
):
super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content, super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
self.result.update({'skipped': True, 'msg': message}) self.result.update({'skipped': True, 'msg': message})
@ -336,7 +394,15 @@ class AnsibleActionSkip(AnsibleAction):
class AnsibleActionFail(AnsibleAction): class AnsibleActionFail(AnsibleAction):
''' an action runtime failure''' ''' an action runtime failure'''
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=False, orig_exc=None, result=None): def __init__(
self,
message="",
obj=None,
show_content=True,
suppress_extended_error=False,
orig_exc=_UNDERLYING_EXC_SENTINEL,
result=None,
):
super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content, super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result) suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
self.result.update({'failed': True, 'msg': message, 'exception': traceback.format_exc()}) self.result.update({'failed': True, 'msg': message, 'exception': traceback.format_exc()})

@ -479,7 +479,7 @@ class TaskExecutor:
# Display the error from the conditional as well to prevent # Display the error from the conditional as well to prevent
# losing information useful for debugging. # losing information useful for debugging.
display.v(to_text(e)) display.v(to_text(e))
raise self._loop_eval_error # pylint: disable=raising-bad-type raise self._loop_eval_error from e # pylint: disable=raising-bad-type
raise raise
# Not skipping, if we had loop error raised earlier we need to raise it now to halt the execution of this task # Not skipping, if we had loop error raised earlier we need to raise it now to halt the execution of this task
@ -495,7 +495,7 @@ class TaskExecutor:
raiseit = False raiseit = False
elif isinstance(context_validation_error, AnsibleParserError): elif isinstance(context_validation_error, AnsibleParserError):
# parser error, might be cause by undef too # parser error, might be cause by undef too
orig_exc = getattr(context_validation_error, 'orig_exc', None) orig_exc = context_validation_error.__cause__
if isinstance(orig_exc, AnsibleUndefinedVariable): if isinstance(orig_exc, AnsibleUndefinedVariable):
raiseit = False raiseit = False
if raiseit: if raiseit:

@ -104,7 +104,7 @@ def g_connect(versions):
data = self._call_galaxy(n_url, method='GET', error_context_msg=error_context_msg, cache=True) data = self._call_galaxy(n_url, method='GET', error_context_msg=error_context_msg, cache=True)
except GalaxyError as new_err: except GalaxyError as new_err:
if new_err.http_code == 404: if new_err.http_code == 404:
raise err raise err from new_err
raise raise
if 'available_versions' not in data: if 'available_versions' not in data:
@ -407,20 +407,23 @@ class GalaxyAPI:
display.vvvv("Calling Galaxy at %s" % url) display.vvvv("Calling Galaxy at %s" % url)
resp = open_url(to_native(url), data=args, validate_certs=self.validate_certs, headers=headers, resp = open_url(to_native(url), data=args, validate_certs=self.validate_certs, headers=headers,
method=method, timeout=self._server_timeout, http_agent=user_agent(), follow_redirects='safe') method=method, timeout=self._server_timeout, http_agent=user_agent(), follow_redirects='safe')
except HTTPError as e: except HTTPError as underlying_http_error:
raise GalaxyError(e, error_context_msg) raise GalaxyError(
except Exception as e: underlying_http_error,
error_context_msg,
) from underlying_http_error
except Exception as underlying_error:
raise AnsibleError( raise AnsibleError(
"Unknown error when attempting to call Galaxy at '%s': %s" % (url, to_native(e)), "Unknown error when attempting to call Galaxy at '%s': %s"
orig_exc=e % (url, to_native(underlying_error)),
) ) from underlying_error
resp_data = to_text(resp.read(), errors='surrogate_or_strict') resp_data = to_text(resp.read(), errors='surrogate_or_strict')
try: try:
data = json.loads(resp_data) data = json.loads(resp_data)
except ValueError: except ValueError as json_value_error:
raise AnsibleError("Failed to parse Galaxy response from '%s' as JSON:\n%s" raise AnsibleError("Failed to parse Galaxy response from '%s' as JSON:\n%s"
% (resp.url, to_native(resp_data))) % (resp.url, to_native(resp_data))) from json_value_error
if cache and self._cache: if cache and self._cache:
path_cache = self._cache[cache_id][cache_key] path_cache = self._cache[cache_id][cache_key]
@ -470,10 +473,16 @@ class GalaxyAPI:
try: try:
resp = open_url(url, data=args, validate_certs=self.validate_certs, method="POST", http_agent=user_agent(), timeout=self._server_timeout) resp = open_url(url, data=args, validate_certs=self.validate_certs, method="POST", http_agent=user_agent(), timeout=self._server_timeout)
except HTTPError as e: except HTTPError as underlying_http_error:
raise GalaxyError(e, 'Attempting to authenticate to galaxy') raise GalaxyError(
except Exception as e: underlying_http_error,
raise AnsibleError('Unable to authenticate to galaxy: %s' % to_native(e), orig_exc=e) 'Attempting to authenticate to galaxy',
) from underlying_http_error
except Exception as underlying_error:
raise AnsibleError(
'Unable to authenticate to galaxy: %s'
% to_native(underlying_error),
) from underlying_error
data = json.loads(to_text(resp.read(), errors='surrogate_or_strict')) data = json.loads(to_text(resp.read(), errors='surrogate_or_strict'))
return data return data
@ -525,8 +534,11 @@ class GalaxyAPI:
role_name = parts[-1] role_name = parts[-1]
if notify: if notify:
display.display("- downloading role '%s', owned by %s" % (role_name, user_name)) display.display("- downloading role '%s', owned by %s" % (role_name, user_name))
except Exception: except Exception as underlying_error:
raise AnsibleError("Invalid role name (%s). Specify role as format: username.rolename" % role_name) raise AnsibleError(
"Invalid role name (%s). Specify role as format: username.rolename"
% role_name,
) from underlying_error
url = _urljoin(self.api_server, self.available_api_versions['v1'], "roles", url = _urljoin(self.api_server, self.available_api_versions['v1'], "roles",
"?owner__username=%s&name=%s" % (user_name, role_name)) "?owner__username=%s&name=%s" % (user_name, role_name))
@ -586,8 +598,11 @@ class GalaxyAPI:
results += data['results'] results += data['results']
done = (data.get('next_link', None) is None) done = (data.get('next_link', None) is None)
return results return results
except Exception as error: except Exception as underlying_error:
raise AnsibleError("Failed to download the %s list: %s" % (what, to_native(error))) raise AnsibleError(
"Failed to download the %s list: %s"
% (what, to_native(underlying_error)),
) from underlying_error
@g_connect(['v1']) @g_connect(['v1'])
def search_roles(self, search, **kwargs): def search_roles(self, search, **kwargs):

@ -485,7 +485,7 @@
that: that:
- unsupported_hash_type.msg == msg - unsupported_hash_type.msg == msg
vars: vars:
msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512" msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512. user must be unicode or bytes, not None"
- name: Verify to_uuid throws on weird namespace - name: Verify to_uuid throws on weird namespace
set_fact: set_fact:

Loading…
Cancel
Save