normalize error handler choices (#84998)
use existing to avoid deprecation cycle normalize test too
This commit is contained in:
@ -16,8 +16,8 @@ class ErrorAction(enum.Enum):
|
||||
"""Action to take when an error is encountered."""
|
||||
|
||||
IGNORE = enum.auto()
|
||||
WARN = enum.auto()
|
||||
FAIL = enum.auto()
|
||||
WARNING = enum.auto()
|
||||
ERROR = enum.auto()
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, setting: str, variables: dict[str, t.Any] | None = None) -> t.Self:
|
||||
@ -75,9 +75,9 @@ class ErrorHandler:
|
||||
yield
|
||||
except args as ex:
|
||||
match self.action:
|
||||
case ErrorAction.WARN:
|
||||
case ErrorAction.WARNING:
|
||||
display.error_as_warning(msg=None, exception=ex)
|
||||
case ErrorAction.FAIL:
|
||||
case ErrorAction.ERROR:
|
||||
raise
|
||||
case _: # ErrorAction.IGNORE
|
||||
pass
|
||||
|
||||
@ -985,12 +985,12 @@ def _maybe_finalize_scalar(o: t.Any) -> t.Any:
|
||||
|
||||
match _TemplateConfig.unknown_type_conversion_handler.action:
|
||||
# we don't want to show the object value, and it can't be Origin-tagged; send the current template value for best effort
|
||||
case ErrorAction.WARN:
|
||||
case ErrorAction.WARNING:
|
||||
display.warning(
|
||||
msg=f'Type {native_type_name(o)!r} is unsupported in variable storage, converting to {native_type_name(target_type)!r}.',
|
||||
obj=TemplateContext.current(optional=True).template_value,
|
||||
)
|
||||
case ErrorAction.FAIL:
|
||||
case ErrorAction.ERROR:
|
||||
raise AnsibleVariableTypeError.from_value(obj=TemplateContext.current(optional=True).template_value)
|
||||
|
||||
return target_type(o)
|
||||
@ -1006,12 +1006,12 @@ def _finalize_fallback_collection(
|
||||
) -> t.Collection[t.Any]:
|
||||
match _TemplateConfig.unknown_type_conversion_handler.action:
|
||||
# we don't want to show the object value, and it can't be Origin-tagged; send the current template value for best effort
|
||||
case ErrorAction.WARN:
|
||||
case ErrorAction.WARNING:
|
||||
display.warning(
|
||||
msg=f'Type {native_type_name(o)!r} is unsupported in variable storage, converting to {native_type_name(target_type)!r}.',
|
||||
obj=TemplateContext.current(optional=True).template_value,
|
||||
)
|
||||
case ErrorAction.FAIL:
|
||||
case ErrorAction.ERROR:
|
||||
raise AnsibleVariableTypeError.from_value(obj=TemplateContext.current(optional=True).template_value)
|
||||
|
||||
return _finalize_collection(o, mode, finalizer, target_type)
|
||||
|
||||
@ -9,6 +9,18 @@ _ANSIBLE_CONNECTION_PATH:
|
||||
- For internal use only.
|
||||
type: path
|
||||
version_added: "2.18"
|
||||
_CALLBACK_DISPATCH_ERROR_BEHAVIOR:
|
||||
name: Callback dispatch error behavior
|
||||
default: warning
|
||||
description:
|
||||
- Action to take when a callback dispatch results in an error.
|
||||
type: choices
|
||||
choices: &basic_error
|
||||
error: issue a 'fatal' error and stop the play
|
||||
warning: issue a warning but continue
|
||||
ignore: just continue silently
|
||||
env: [ { name: _ANSIBLE_CALLBACK_DISPATCH_ERROR_BEHAVIOR } ]
|
||||
version_added: '2.19'
|
||||
ALLOW_BROKEN_CONDITIONALS:
|
||||
# This config option will be deprecated once it no longer has any effect (2.23).
|
||||
name: Allow broken conditionals
|
||||
@ -224,18 +236,6 @@ CACHE_PLUGIN_TIMEOUT:
|
||||
- {key: fact_caching_timeout, section: defaults}
|
||||
type: integer
|
||||
yaml: {key: facts.cache.timeout}
|
||||
_CALLBACK_DISPATCH_ERROR_BEHAVIOR:
|
||||
name: Callback dispatch error behavior
|
||||
default: warn
|
||||
description:
|
||||
- Action to take when a callback dispatch results in an error.
|
||||
type: choices
|
||||
choices: &choices_ignore_warn_fail
|
||||
- ignore
|
||||
- warn
|
||||
- fail
|
||||
env: [ { name: _ANSIBLE_CALLBACK_DISPATCH_ERROR_BEHAVIOR } ]
|
||||
version_added: '2.19'
|
||||
COLLECTIONS_SCAN_SYS_PATH:
|
||||
name: Scan PYTHONPATH for installed collections
|
||||
description: A boolean to enable or disable scanning the sys.path for installed collections.
|
||||
@ -268,10 +268,7 @@ COLLECTIONS_ON_ANSIBLE_VERSION_MISMATCH:
|
||||
- When a collection is loaded that does not support the running Ansible version (with the collection metadata key `requires_ansible`).
|
||||
env: [{name: ANSIBLE_COLLECTIONS_ON_ANSIBLE_VERSION_MISMATCH}]
|
||||
ini: [{key: collections_on_ansible_version_mismatch, section: defaults}]
|
||||
choices: &basic_error
|
||||
error: issue a 'fatal' error and stop the play
|
||||
warning: issue a warning but continue
|
||||
ignore: just continue silently
|
||||
choices: *basic_error
|
||||
default: warning
|
||||
COLOR_CHANGED:
|
||||
name: Color for 'changed' task status
|
||||
@ -2058,13 +2055,13 @@ TASK_TIMEOUT:
|
||||
version_added: '2.10'
|
||||
_TEMPLAR_UNKNOWN_TYPE_CONVERSION:
|
||||
name: Templar unknown type conversion behavior
|
||||
default: warn
|
||||
default: warning
|
||||
description:
|
||||
- Action to take when an unknown type is converted for variable storage during template finalization.
|
||||
- This setting has no effect on the inability to store unsupported variable types as the result of templating.
|
||||
- Experimental diagnostic feature, subject to change.
|
||||
type: choices
|
||||
choices: *choices_ignore_warn_fail
|
||||
choices: *basic_error
|
||||
env: [{name: _ANSIBLE_TEMPLAR_UNKNOWN_TYPE_CONVERSION}]
|
||||
version_added: '2.19'
|
||||
_TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED:
|
||||
@ -2074,7 +2071,7 @@ _TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED:
|
||||
- Action to take when an unknown type is encountered inside a template pipeline.
|
||||
- Experimental diagnostic feature, subject to change.
|
||||
type: choices
|
||||
choices: *choices_ignore_warn_fail
|
||||
choices: *basic_error
|
||||
env: [{name: _ANSIBLE_TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED}]
|
||||
version_added: '2.19'
|
||||
_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR:
|
||||
@ -2086,7 +2083,7 @@ _TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR:
|
||||
- This setting has no effect on expressions.
|
||||
- Experimental diagnostic feature, subject to change.
|
||||
type: choices
|
||||
choices: *choices_ignore_warn_fail
|
||||
choices: *basic_error
|
||||
env: [{name: _ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR}]
|
||||
version_added: '2.19'
|
||||
WORKER_SHUTDOWN_POLL_COUNT:
|
||||
|
||||
@ -10,7 +10,7 @@ ansible --task-timeout 5 localhost -m command -a '{"cmd": "whoami"}' | grep 'rc=
|
||||
|
||||
# ensure that legacy deserializer behaves as expected on JSON CLI args (https://github.com/ansible/ansible/issues/82600)
|
||||
# also ensure that various templated args function (non-exhaustive)
|
||||
_ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR=warn ansible '{{"localhost"}}' -m '{{"debug"}}' -a var=fromcli -e '{"fromcli":{"no_trust":{"__ansible_unsafe":"{{\"hello\"}}"},"trust":"{{ 1 }}"}}' > "${OUTPUT_DIR}/output.txt" 2>&1
|
||||
_ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR=warning ansible '{{"localhost"}}' -m '{{"debug"}}' -a var=fromcli -e '{"fromcli":{"no_trust":{"__ansible_unsafe":"{{\"hello\"}}"},"trust":"{{ 1 }}"}}' > "${OUTPUT_DIR}/output.txt" 2>&1
|
||||
grep '"no_trust": "{{."hello."}}"' "${OUTPUT_DIR}/output.txt" # ensure that the template was not rendered
|
||||
grep '"trust": 1' "${OUTPUT_DIR}/output.txt" # ensure that the trusted template was rendered
|
||||
grep "Encountered untrusted template" "${OUTPUT_DIR}/output.txt" # look for the untrusted template warning text
|
||||
|
||||
@ -6,7 +6,7 @@ unset ANSIBLE_DEPRECATION_WARNINGS
|
||||
|
||||
ansible-playbook untrusted_propagation.yml "$@" -e output_dir="${OUTPUT_DIR}"
|
||||
|
||||
ANSIBLE_CALLBACK_FORMAT_PRETTY=0 ANSIBLE_WRAP_STDERR=0 _ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR=warn ansible-playbook -i hosts output_tests.yml -vvv 2>&1 | tee output.txt
|
||||
ANSIBLE_CALLBACK_FORMAT_PRETTY=0 ANSIBLE_WRAP_STDERR=0 _ANSIBLE_TEMPLAR_UNTRUSTED_TEMPLATE_BEHAVIOR=warning ansible-playbook -i hosts output_tests.yml -vvv 2>&1 | tee output.txt
|
||||
|
||||
../playbook_output_validator/filter.py actual_stdout.txt actual_stderr.txt < output.txt
|
||||
|
||||
|
||||
@ -98,18 +98,18 @@ class TestTemplarTemplate(BaseTemplar, unittest.TestCase):
|
||||
"""Ensure template trust check failures default to fatal for unit tests (set in units/conftest.py)"""
|
||||
from ansible._internal._templating._engine import TemplateTrustCheckFailedError
|
||||
|
||||
assert _TemplateConfig.untrusted_template_handler.action is ErrorAction.FAIL
|
||||
assert _TemplateConfig.untrusted_template_handler.action is ErrorAction.ERROR
|
||||
|
||||
with pytest.raises(TemplateTrustCheckFailedError):
|
||||
self.templar.template("{{ i_am_not_trusted }}")
|
||||
|
||||
def test_trust_fail_warning_behavior(self):
|
||||
"""Validate that trust checks are non-fatal when TemplateConfig.untrusted_template_handler is set to `ErrorAction.WARN`."""
|
||||
"""Validate that trust checks are non-fatal when TemplateConfig.untrusted_template_handler is set to `ErrorAction.WARNING`."""
|
||||
untrusted_template = "{{ i_am_not_trusted }}"
|
||||
|
||||
assert hasattr(_TemplateConfig, 'untrusted_template_handler')
|
||||
|
||||
with (unittest.mock.patch.object(_TemplateConfig, 'untrusted_template_handler', ErrorHandler(ErrorAction.WARN)),
|
||||
with (unittest.mock.patch.object(_TemplateConfig, 'untrusted_template_handler', ErrorHandler(ErrorAction.WARNING)),
|
||||
unittest.mock.patch.object(Display, 'error_as_warning', return_value=None) as mock_warning):
|
||||
assert self.templar.template(untrusted_template) is untrusted_template
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ else:
|
||||
|
||||
# Ensure unit tests fail when encountering untrusted templates to reduce mistakes in tests.
|
||||
# Tests that need to ignore or warn on untrusted templates will need to override this setting.
|
||||
_TemplateConfig.untrusted_template_handler = ErrorHandler(ErrorAction.FAIL)
|
||||
_TemplateConfig.untrusted_template_handler = ErrorHandler(ErrorAction.ERROR)
|
||||
|
||||
from .controller_only_conftest import * # pylint: disable=wildcard-import,unused-wildcard-import
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ def test_skippable_non_skip_exception() -> None:
|
||||
assert err.value is ex_to_raise
|
||||
|
||||
|
||||
@pytest.mark.parametrize("error_action", (ErrorAction.IGNORE, ErrorAction.WARN, ErrorAction.FAIL))
|
||||
@pytest.mark.parametrize("error_action", (ErrorAction.IGNORE, ErrorAction.WARNING, ErrorAction.ERROR))
|
||||
def test_skip_on_ignore_missing_skippable(error_action: ErrorAction) -> None:
|
||||
"""Verify that a `_SkipException` is raised when `skip_on_ignore=True` and no `Skippable` context was used to suppress it."""
|
||||
body_ran = False
|
||||
@ -87,20 +87,20 @@ def test_ignore_passes_other_exceptions() -> None:
|
||||
|
||||
@pytest.mark.parametrize("exception_type", (RuntimeError, NotImplementedError))
|
||||
def test_warn_success(exception_type: type[Exception], mocker: pytest_mock.MockerFixture) -> None:
|
||||
"""Verify that `ErrorAction.WARN` eats the specified error type and calls `error_as_warning` with the exception instance raised."""
|
||||
"""Verify that `ErrorAction.WARNING` eats the specified error type and calls `error_as_warning` with the exception instance raised."""
|
||||
eaw = mocker.patch.object(Display(), 'error_as_warning')
|
||||
with ErrorHandler(ErrorAction.WARN).handle(RuntimeError, NotImplementedError):
|
||||
with ErrorHandler(ErrorAction.WARNING).handle(RuntimeError, NotImplementedError):
|
||||
raise exception_type()
|
||||
|
||||
assert isinstance(eaw.call_args.kwargs['exception'], exception_type)
|
||||
|
||||
|
||||
def test_warn_passes_other_exceptions(mocker: pytest_mock.MockerFixture) -> None:
|
||||
"""Verify that `ErrorAction.WARN` does not suppress exception types not passed to `handle`, and that `error_as_warning` is not called for them."""
|
||||
"""Verify that `ErrorAction.WARNING` does not suppress exception types not passed to `handle`, and that `error_as_warning` is not called for them."""
|
||||
eaw = mocker.patch.object(Display(), 'error_as_warning')
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
with ErrorHandler(ErrorAction.WARN).handle(TypeError, ValueError):
|
||||
with ErrorHandler(ErrorAction.WARNING).handle(TypeError, ValueError):
|
||||
raise NotImplementedError()
|
||||
|
||||
assert not eaw.called
|
||||
@ -108,9 +108,9 @@ def test_warn_passes_other_exceptions(mocker: pytest_mock.MockerFixture) -> None
|
||||
|
||||
@pytest.mark.parametrize("exception_type", (AttributeError, NotImplementedError, ValueError))
|
||||
def test_fail(exception_type: type[Exception]) -> None:
|
||||
"""Verify that `ErrorAction.FAIL` passes through all exception types, regardless of what was passed to `handle`."""
|
||||
"""Verify that `ErrorAction.ERROR` passes through all exception types, regardless of what was passed to `handle`."""
|
||||
with pytest.raises(exception_type):
|
||||
with ErrorHandler(ErrorAction.FAIL).handle(AttributeError, NotImplementedError):
|
||||
with ErrorHandler(ErrorAction.ERROR).handle(AttributeError, NotImplementedError):
|
||||
raise exception_type()
|
||||
|
||||
|
||||
@ -121,7 +121,7 @@ def test_no_exceptions_to_handle():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ('ignore', 'warn', 'fail'))
|
||||
@pytest.mark.parametrize("value", ('ignore', 'warning', 'error'))
|
||||
def test_from_config_env_success(value: str, mocker: pytest_mock.MockerFixture) -> None:
|
||||
"""Verify that `from_config` correctly creates handlers with the requested error action config string."""
|
||||
mocker.patch.dict(os.environ, dict(_ANSIBLE_CALLBACK_DISPATCH_ERROR_BEHAVIOR=value))
|
||||
|
||||
Reference in New Issue
Block a user