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
from collections.abc import Sequence
from warnings import warn as _emit_warning
from ansible.errors.yaml_strings import (
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
_UNDERLYING_EXC_SENTINEL = Exception()
"""A sentinel object for use as a default for arguments in callables."""
class AnsibleError(Exception):
'''
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.
'''
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)
self._show_content = show_content
self._suppress_extended_error = suppress_extended_error
self._message = to_native(message)
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
def message(self):
@ -72,8 +105,8 @@ class AnsibleError(Exception):
message.append(
'\n\n%s' % to_native(extended_error)
)
elif self.orig_exc:
message.append('. %s' % to_native(self.orig_exc))
elif self.__cause__:
message.append('. %s' % to_native(self.__cause__))
return ''.join(message)
@ -285,7 +318,16 @@ class AnsibleUndefinedVariable(AnsibleTemplateError):
class AnsibleFileNotFound(AnsibleRuntimeError):
''' 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.paths = paths
@ -315,7 +357,15 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
class AnsibleAction(AnsibleRuntimeError):
''' 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,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
@ -328,7 +378,15 @@ class AnsibleAction(AnsibleRuntimeError):
class AnsibleActionSkip(AnsibleAction):
''' 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,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
self.result.update({'skipped': True, 'msg': message})
@ -336,7 +394,15 @@ class AnsibleActionSkip(AnsibleAction):
class AnsibleActionFail(AnsibleAction):
''' 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,
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
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
# losing information useful for debugging.
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
# 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
elif isinstance(context_validation_error, AnsibleParserError):
# 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):
raiseit = False
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)
except GalaxyError as new_err:
if new_err.http_code == 404:
raise err
raise err from new_err
raise
if 'available_versions' not in data:
@ -407,20 +407,23 @@ class GalaxyAPI:
display.vvvv("Calling Galaxy at %s" % url)
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')
except HTTPError as e:
raise GalaxyError(e, error_context_msg)
except Exception as e:
except HTTPError as underlying_http_error:
raise GalaxyError(
underlying_http_error,
error_context_msg,
) from underlying_http_error
except Exception as underlying_error:
raise AnsibleError(
"Unknown error when attempting to call Galaxy at '%s': %s" % (url, to_native(e)),
orig_exc=e
)
"Unknown error when attempting to call Galaxy at '%s': %s"
% (url, to_native(underlying_error)),
) from underlying_error
resp_data = to_text(resp.read(), errors='surrogate_or_strict')
try:
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"
% (resp.url, to_native(resp_data)))
% (resp.url, to_native(resp_data))) from json_value_error
if cache and self._cache:
path_cache = self._cache[cache_id][cache_key]
@ -470,10 +473,16 @@ class GalaxyAPI:
try:
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:
raise GalaxyError(e, 'Attempting to authenticate to galaxy')
except Exception as e:
raise AnsibleError('Unable to authenticate to galaxy: %s' % to_native(e), orig_exc=e)
except HTTPError as underlying_http_error:
raise GalaxyError(
underlying_http_error,
'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'))
return data
@ -525,8 +534,11 @@ class GalaxyAPI:
role_name = parts[-1]
if notify:
display.display("- downloading role '%s', owned by %s" % (role_name, user_name))
except Exception:
raise AnsibleError("Invalid role name (%s). Specify role as format: username.rolename" % role_name)
except Exception as underlying_error:
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",
"?owner__username=%s&name=%s" % (user_name, role_name))
@ -586,8 +598,11 @@ class GalaxyAPI:
results += data['results']
done = (data.get('next_link', None) is None)
return results
except Exception as error:
raise AnsibleError("Failed to download the %s list: %s" % (what, to_native(error)))
except Exception as underlying_error:
raise AnsibleError(
"Failed to download the %s list: %s"
% (what, to_native(underlying_error)),
) from underlying_error
@g_connect(['v1'])
def search_roles(self, search, **kwargs):

@ -485,7 +485,7 @@
that:
- unsupported_hash_type.msg == msg
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
set_fact:

Loading…
Cancel
Save