ansible-core 2.19.0b1__py3-none-any.whl → 2.19.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansible/_internal/_collection_proxy.py +47 -0
- ansible/_internal/_errors/_handler.py +4 -4
- ansible/_internal/_json/__init__.py +47 -4
- ansible/_internal/_json/_profiles/_legacy.py +2 -3
- ansible/_internal/_templating/_jinja_bits.py +4 -4
- ansible/_internal/_templating/_jinja_plugins.py +5 -11
- ansible/cli/__init__.py +9 -3
- ansible/cli/doc.py +14 -7
- ansible/config/base.yml +17 -20
- ansible/executor/process/worker.py +31 -26
- ansible/executor/task_executor.py +32 -23
- ansible/executor/task_queue_manager.py +62 -52
- ansible/executor/task_result.py +168 -72
- ansible/inventory/manager.py +2 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +4 -6
- ansible/module_utils/common/warnings.py +1 -1
- ansible/parsing/utils/jsonify.py +40 -0
- ansible/parsing/yaml/objects.py +16 -5
- ansible/playbook/included_file.py +25 -12
- ansible/plugins/callback/__init__.py +173 -86
- ansible/plugins/callback/default.py +79 -79
- ansible/plugins/callback/junit.py +20 -19
- ansible/plugins/callback/minimal.py +17 -17
- ansible/plugins/callback/oneline.py +16 -15
- ansible/plugins/callback/tree.py +6 -5
- ansible/plugins/filter/core.py +8 -1
- ansible/plugins/strategy/__init__.py +70 -76
- ansible/plugins/strategy/free.py +4 -4
- ansible/plugins/strategy/linear.py +11 -9
- ansible/plugins/test/core.py +1 -1
- ansible/release.py +1 -1
- ansible/template/__init__.py +7 -5
- ansible/utils/display.py +10 -10
- ansible/utils/vars.py +23 -0
- ansible/vars/clean.py +1 -1
- ansible/vars/manager.py +2 -19
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/RECORD +48 -46
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/COPYING +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@
|
|
17
17
|
|
18
18
|
from __future__ import annotations
|
19
19
|
|
20
|
+
import dataclasses
|
20
21
|
import os
|
21
22
|
import sys
|
22
23
|
import tempfile
|
@@ -31,7 +32,7 @@ from ansible.errors import AnsibleError, ExitCode, AnsibleCallbackError
|
|
31
32
|
from ansible._internal._errors._handler import ErrorHandler
|
32
33
|
from ansible.executor.play_iterator import PlayIterator
|
33
34
|
from ansible.executor.stats import AggregateStats
|
34
|
-
from ansible.executor.task_result import
|
35
|
+
from ansible.executor.task_result import _RawTaskResult, _WireTaskResult
|
35
36
|
from ansible.inventory.data import InventoryData
|
36
37
|
from ansible.module_utils.six import string_types
|
37
38
|
from ansible.module_utils.common.text.converters import to_native
|
@@ -47,7 +48,8 @@ from ansible.utils.display import Display
|
|
47
48
|
from ansible.utils.lock import lock_decorator
|
48
49
|
from ansible.utils.multiprocessing import context as multiprocessing_context
|
49
50
|
|
50
|
-
|
51
|
+
if t.TYPE_CHECKING:
|
52
|
+
from ansible.executor.process.worker import WorkerProcess
|
51
53
|
|
52
54
|
__all__ = ['TaskQueueManager']
|
53
55
|
|
@@ -57,12 +59,13 @@ STDERR_FILENO = 2
|
|
57
59
|
|
58
60
|
display = Display()
|
59
61
|
|
62
|
+
_T = t.TypeVar('_T')
|
60
63
|
|
64
|
+
|
65
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
61
66
|
class CallbackSend:
|
62
|
-
|
63
|
-
|
64
|
-
self.args = args
|
65
|
-
self.kwargs = kwargs
|
67
|
+
method_name: str
|
68
|
+
wire_task_result: _WireTaskResult
|
66
69
|
|
67
70
|
|
68
71
|
class DisplaySend:
|
@@ -72,7 +75,7 @@ class DisplaySend:
|
|
72
75
|
self.kwargs = kwargs
|
73
76
|
|
74
77
|
|
75
|
-
@dataclass
|
78
|
+
@dataclasses.dataclass
|
76
79
|
class PromptSend:
|
77
80
|
worker_id: int
|
78
81
|
prompt: str
|
@@ -87,19 +90,11 @@ class FinalQueue(multiprocessing.queues.SimpleQueue):
|
|
87
90
|
kwargs['ctx'] = multiprocessing_context
|
88
91
|
super().__init__(*args, **kwargs)
|
89
92
|
|
90
|
-
def send_callback(self, method_name
|
91
|
-
self.put(
|
92
|
-
CallbackSend(method_name, *args, **kwargs),
|
93
|
-
)
|
93
|
+
def send_callback(self, method_name: str, task_result: _RawTaskResult) -> None:
|
94
|
+
self.put(CallbackSend(method_name=method_name, wire_task_result=task_result.as_wire_task_result()))
|
94
95
|
|
95
|
-
def send_task_result(self,
|
96
|
-
|
97
|
-
tr = args[0]
|
98
|
-
else:
|
99
|
-
tr = TaskResult(*args, **kwargs)
|
100
|
-
self.put(
|
101
|
-
tr,
|
102
|
-
)
|
96
|
+
def send_task_result(self, task_result: _RawTaskResult) -> None:
|
97
|
+
self.put(task_result.as_wire_task_result())
|
103
98
|
|
104
99
|
def send_display(self, method, *args, **kwargs):
|
105
100
|
self.put(
|
@@ -194,11 +189,8 @@ class TaskQueueManager:
|
|
194
189
|
# plugins for inter-process locking.
|
195
190
|
self._connection_lockfile = tempfile.TemporaryFile()
|
196
191
|
|
197
|
-
def _initialize_processes(self, num):
|
198
|
-
self._workers = []
|
199
|
-
|
200
|
-
for i in range(num):
|
201
|
-
self._workers.append(None)
|
192
|
+
def _initialize_processes(self, num: int) -> None:
|
193
|
+
self._workers: list[WorkerProcess | None] = [None] * num
|
202
194
|
|
203
195
|
def load_callbacks(self):
|
204
196
|
"""
|
@@ -438,54 +430,72 @@ class TaskQueueManager:
|
|
438
430
|
defunct = True
|
439
431
|
return defunct
|
440
432
|
|
433
|
+
@staticmethod
|
434
|
+
def _first_arg_of_type(value_type: t.Type[_T], args: t.Sequence) -> _T | None:
|
435
|
+
return next((arg for arg in args if isinstance(arg, value_type)), None)
|
436
|
+
|
441
437
|
@lock_decorator(attr='_callback_lock')
|
442
438
|
def send_callback(self, method_name, *args, **kwargs):
|
443
439
|
# We always send events to stdout callback first, rest should follow config order
|
444
440
|
for callback_plugin in [self._stdout_callback] + self._callback_plugins:
|
445
441
|
# a plugin that set self.disabled to True will not be called
|
446
442
|
# see osx_say.py example for such a plugin
|
447
|
-
if
|
443
|
+
if callback_plugin.disabled:
|
448
444
|
continue
|
449
445
|
|
450
446
|
# a plugin can opt in to implicit tasks (such as meta). It does this
|
451
447
|
# by declaring self.wants_implicit_tasks = True.
|
452
|
-
wants_implicit_tasks
|
448
|
+
if not callback_plugin.wants_implicit_tasks and (task_arg := self._first_arg_of_type(Task, args)) and task_arg.implicit:
|
449
|
+
continue
|
453
450
|
|
454
451
|
# try to find v2 method, fallback to v1 method, ignore callback if no method found
|
455
452
|
methods = []
|
453
|
+
|
456
454
|
for possible in [method_name, 'v2_on_any']:
|
457
|
-
|
458
|
-
if gotit is None:
|
459
|
-
gotit = getattr(callback_plugin, possible.removeprefix('v2_'), None)
|
460
|
-
if gotit is not None:
|
461
|
-
methods.append(gotit)
|
462
|
-
|
463
|
-
# send clean copies
|
464
|
-
new_args = []
|
465
|
-
|
466
|
-
# If we end up being given an implicit task, we'll set this flag in
|
467
|
-
# the loop below. If the plugin doesn't care about those, then we
|
468
|
-
# check and continue to the next iteration of the outer loop.
|
469
|
-
is_implicit_task = False
|
470
|
-
|
471
|
-
for arg in args:
|
472
|
-
# FIXME: add play/task cleaners
|
473
|
-
if isinstance(arg, TaskResult):
|
474
|
-
new_args.append(arg.clean_copy())
|
475
|
-
# elif isinstance(arg, Play):
|
476
|
-
# elif isinstance(arg, Task):
|
477
|
-
else:
|
478
|
-
new_args.append(arg)
|
455
|
+
method = getattr(callback_plugin, possible, None)
|
479
456
|
|
480
|
-
if
|
481
|
-
|
457
|
+
if method is None:
|
458
|
+
method = getattr(callback_plugin, possible.removeprefix('v2_'), None)
|
482
459
|
|
483
|
-
|
484
|
-
|
460
|
+
if method is not None:
|
461
|
+
display.deprecated(
|
462
|
+
msg='The v1 callback API is deprecated.',
|
463
|
+
version='2.23',
|
464
|
+
help_text='Use `v2_` prefixed callback methods instead.',
|
465
|
+
)
|
466
|
+
|
467
|
+
if method is not None and not getattr(method, '_base_impl', False): # don't bother dispatching to the base impls
|
468
|
+
if possible == 'v2_on_any':
|
469
|
+
display.deprecated(
|
470
|
+
msg='The `v2_on_any` callback method is deprecated.',
|
471
|
+
version='2.23',
|
472
|
+
help_text='Use event-specific callback methods instead.',
|
473
|
+
)
|
474
|
+
|
475
|
+
methods.append(method)
|
485
476
|
|
486
477
|
for method in methods:
|
478
|
+
# send clean copies
|
479
|
+
new_args = []
|
480
|
+
|
481
|
+
for arg in args:
|
482
|
+
# FIXME: add play/task cleaners
|
483
|
+
if isinstance(arg, _RawTaskResult):
|
484
|
+
copied_tr = arg.as_callback_task_result()
|
485
|
+
new_args.append(copied_tr)
|
486
|
+
# this state hack requires that no callback ever accepts > 1 TaskResult object
|
487
|
+
callback_plugin._current_task_result = copied_tr
|
488
|
+
else:
|
489
|
+
new_args.append(arg)
|
490
|
+
|
487
491
|
with self._callback_dispatch_error_handler.handle(AnsibleCallbackError):
|
488
492
|
try:
|
489
493
|
method(*new_args, **kwargs)
|
494
|
+
except AssertionError:
|
495
|
+
# Using an `assert` in integration tests is useful.
|
496
|
+
# Production code should never use `assert` or raise `AssertionError`.
|
497
|
+
raise
|
490
498
|
except Exception as ex:
|
491
499
|
raise AnsibleCallbackError(f"Callback dispatch {method_name!r} failed for plugin {callback_plugin._load_name!r}.") from ex
|
500
|
+
|
501
|
+
callback_plugin._current_task_result = None
|
ansible/executor/task_result.py
CHANGED
@@ -4,15 +4,24 @@
|
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
7
|
+
import collections.abc as _c
|
8
|
+
import dataclasses
|
9
|
+
import functools
|
7
10
|
import typing as t
|
8
11
|
|
9
|
-
from ansible import constants
|
10
|
-
from ansible.
|
12
|
+
from ansible import constants
|
13
|
+
from ansible.utils import vars as _vars
|
11
14
|
from ansible.vars.clean import module_response_deepcopy, strip_internal_keys
|
15
|
+
from ansible.module_utils.common import messages as _messages
|
16
|
+
from ansible._internal import _collection_proxy
|
17
|
+
|
18
|
+
if t.TYPE_CHECKING:
|
19
|
+
from ansible.inventory.host import Host
|
20
|
+
from ansible.playbook.task import Task
|
12
21
|
|
13
22
|
_IGNORE = ('failed', 'skipped')
|
14
|
-
_PRESERVE =
|
15
|
-
_SUB_PRESERVE = {'_ansible_delegated_vars':
|
23
|
+
_PRESERVE = {'attempts', 'changed', 'retries', '_ansible_no_log'}
|
24
|
+
_SUB_PRESERVE = {'_ansible_delegated_vars': {'ansible_host', 'ansible_port', 'ansible_user', 'ansible_connection'}}
|
16
25
|
|
17
26
|
# stuff callbacks need
|
18
27
|
CLEAN_EXCEPTIONS = (
|
@@ -23,61 +32,120 @@ CLEAN_EXCEPTIONS = (
|
|
23
32
|
)
|
24
33
|
|
25
34
|
|
26
|
-
|
35
|
+
@t.final
|
36
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
37
|
+
class _WireTaskResult:
|
38
|
+
"""A thin version of `_RawTaskResult` which can be sent over the worker queue."""
|
39
|
+
|
40
|
+
host_name: str
|
41
|
+
task_uuid: str
|
42
|
+
return_data: _c.MutableMapping[str, object]
|
43
|
+
task_fields: _c.Mapping[str, object]
|
44
|
+
|
45
|
+
|
46
|
+
class _BaseTaskResult:
|
27
47
|
"""
|
28
48
|
This class is responsible for interpreting the resulting data
|
29
49
|
from an executed task, and provides helper methods for determining
|
30
50
|
the result of a given task.
|
31
51
|
"""
|
32
52
|
|
33
|
-
def __init__(self, host, task, return_data, task_fields
|
34
|
-
self.
|
35
|
-
self.
|
53
|
+
def __init__(self, host: Host, task: Task, return_data: _c.MutableMapping[str, t.Any], task_fields: _c.Mapping[str, t.Any]) -> None:
|
54
|
+
self.__host = host
|
55
|
+
self.__task = task
|
56
|
+
self._return_data = return_data # FIXME: this should be immutable, but strategy result processing mutates it in some corner cases
|
57
|
+
self.__task_fields = task_fields
|
36
58
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
59
|
+
@property
|
60
|
+
def host(self) -> Host:
|
61
|
+
"""The host associated with this result."""
|
62
|
+
return self.__host
|
41
63
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
64
|
+
@property
|
65
|
+
def _host(self) -> Host:
|
66
|
+
"""Use the `host` property when supporting only ansible-core 2.19 or later."""
|
67
|
+
# deprecated: description='Deprecate `_host` in favor of `host`' core_version='2.23'
|
68
|
+
return self.__host
|
69
|
+
|
70
|
+
@property
|
71
|
+
def task(self) -> Task:
|
72
|
+
"""The task associated with this result."""
|
73
|
+
return self.__task
|
74
|
+
|
75
|
+
@property
|
76
|
+
def _task(self) -> Task:
|
77
|
+
"""Use the `task` property when supporting only ansible-core 2.19 or later."""
|
78
|
+
# deprecated: description='Deprecate `_task` in favor of `task`' core_version='2.23'
|
79
|
+
return self.__task
|
80
|
+
|
81
|
+
@property
|
82
|
+
def task_fields(self) -> _c.Mapping[str, t.Any]:
|
83
|
+
"""The task fields associated with this result."""
|
84
|
+
return self.__task_fields
|
85
|
+
|
86
|
+
@property
|
87
|
+
def _task_fields(self) -> _c.Mapping[str, t.Any]:
|
88
|
+
"""Use the `task_fields` property when supporting only ansible-core 2.19 or later."""
|
89
|
+
# deprecated: description='Deprecate `_task_fields` in favor of `task`' core_version='2.23'
|
90
|
+
return self.__task_fields
|
91
|
+
|
92
|
+
@property
|
93
|
+
def exception(self) -> _messages.ErrorSummary | None:
|
94
|
+
"""The error from this task result, if any."""
|
95
|
+
return self._return_data.get('exception')
|
96
|
+
|
97
|
+
@property
|
98
|
+
def warnings(self) -> _c.Sequence[_messages.WarningSummary]:
|
99
|
+
"""The warnings for this task, if any."""
|
100
|
+
return _collection_proxy.SequenceProxy(self._return_data.get('warnings') or [])
|
101
|
+
|
102
|
+
@property
|
103
|
+
def deprecations(self) -> _c.Sequence[_messages.DeprecationSummary]:
|
104
|
+
"""The deprecation warnings for this task, if any."""
|
105
|
+
return _collection_proxy.SequenceProxy(self._return_data.get('deprecations') or [])
|
106
|
+
|
107
|
+
@property
|
108
|
+
def _loop_results(self) -> list[_c.MutableMapping[str, t.Any]]:
|
109
|
+
"""Return a list of loop results. If no loop results are present, an empty list is returned."""
|
110
|
+
results = self._return_data.get('results')
|
111
|
+
|
112
|
+
if not isinstance(results, list):
|
113
|
+
return []
|
114
|
+
|
115
|
+
return results
|
46
116
|
|
47
117
|
@property
|
48
|
-
def task_name(self):
|
49
|
-
return self.
|
118
|
+
def task_name(self) -> str:
|
119
|
+
return str(self.task_fields.get('name', '')) or self.task.get_name()
|
50
120
|
|
51
|
-
def is_changed(self):
|
121
|
+
def is_changed(self) -> bool:
|
52
122
|
return self._check_key('changed')
|
53
123
|
|
54
|
-
def is_skipped(self):
|
55
|
-
|
56
|
-
if 'results' in self._result:
|
57
|
-
results = self._result['results']
|
124
|
+
def is_skipped(self) -> bool:
|
125
|
+
if self._loop_results:
|
58
126
|
# Loop tasks are only considered skipped if all items were skipped.
|
59
127
|
# some squashed results (eg, dnf) are not dicts and can't be skipped individually
|
60
|
-
if
|
128
|
+
if all(isinstance(loop_res, dict) and loop_res.get('skipped', False) for loop_res in self._loop_results):
|
61
129
|
return True
|
62
130
|
|
63
131
|
# regular tasks and squashed non-dict results
|
64
|
-
return self.
|
132
|
+
return bool(self._return_data.get('skipped', False))
|
65
133
|
|
66
|
-
def is_failed(self):
|
67
|
-
if 'failed_when_result' in self.
|
68
|
-
'results' in self._result and True in [True for x in self._result['results'] if 'failed_when_result' in x]:
|
134
|
+
def is_failed(self) -> bool:
|
135
|
+
if 'failed_when_result' in self._return_data or any(isinstance(loop_res, dict) and 'failed_when_result' in loop_res for loop_res in self._loop_results):
|
69
136
|
return self._check_key('failed_when_result')
|
70
|
-
else:
|
71
|
-
return self._check_key('failed')
|
72
137
|
|
73
|
-
|
138
|
+
return self._check_key('failed')
|
139
|
+
|
140
|
+
def is_unreachable(self) -> bool:
|
74
141
|
return self._check_key('unreachable')
|
75
142
|
|
76
|
-
def needs_debugger(self, globally_enabled=False):
|
77
|
-
_debugger = self.
|
78
|
-
_ignore_errors =
|
143
|
+
def needs_debugger(self, globally_enabled: bool = False) -> bool:
|
144
|
+
_debugger = self.task_fields.get('debugger')
|
145
|
+
_ignore_errors = constants.TASK_DEBUGGER_IGNORE_ERRORS and self.task_fields.get('ignore_errors')
|
79
146
|
|
80
147
|
ret = False
|
148
|
+
|
81
149
|
if globally_enabled and ((self.is_failed() and not _ignore_errors) or self.is_unreachable()):
|
82
150
|
ret = True
|
83
151
|
|
@@ -94,68 +162,96 @@ class TaskResult:
|
|
94
162
|
|
95
163
|
return ret
|
96
164
|
|
97
|
-
def _check_key(self, key):
|
98
|
-
"""
|
165
|
+
def _check_key(self, key: str) -> bool:
|
166
|
+
"""Fetch a specific named boolean value from the result; if missing, a logical OR of the value from nested loop results; False for non-loop results."""
|
167
|
+
if (value := self._return_data.get(key, ...)) is not ...:
|
168
|
+
return bool(value)
|
99
169
|
|
100
|
-
|
101
|
-
return self._result.get(key, False)
|
102
|
-
else:
|
103
|
-
flag = False
|
104
|
-
for res in self._result.get('results', []):
|
105
|
-
if isinstance(res, dict):
|
106
|
-
flag |= res.get(key, False)
|
107
|
-
return flag
|
170
|
+
return any(isinstance(result, dict) and result.get(key) for result in self._loop_results)
|
108
171
|
|
109
|
-
def clean_copy(self):
|
110
172
|
|
111
|
-
|
173
|
+
@t.final
|
174
|
+
class _RawTaskResult(_BaseTaskResult):
|
175
|
+
def as_wire_task_result(self) -> _WireTaskResult:
|
176
|
+
"""Return a `_WireTaskResult` from this instance."""
|
177
|
+
return _WireTaskResult(
|
178
|
+
host_name=self.host.name,
|
179
|
+
task_uuid=self.task._uuid,
|
180
|
+
return_data=self._return_data,
|
181
|
+
task_fields=self.task_fields,
|
182
|
+
)
|
112
183
|
|
113
|
-
|
114
|
-
|
184
|
+
def as_callback_task_result(self) -> CallbackTaskResult:
|
185
|
+
"""Return a `CallbackTaskResult` from this instance."""
|
186
|
+
ignore: tuple[str, ...]
|
115
187
|
|
116
188
|
# statuses are already reflected on the event type
|
117
|
-
if
|
189
|
+
if self.task and self.task.action in constants._ACTION_DEBUG:
|
118
190
|
# debug is verbose by default to display vars, no need to add invocation
|
119
191
|
ignore = _IGNORE + ('invocation',)
|
120
192
|
else:
|
121
193
|
ignore = _IGNORE
|
122
194
|
|
123
|
-
subset = {}
|
195
|
+
subset: dict[str, dict[str, object]] = {}
|
196
|
+
|
124
197
|
# preserve subset for later
|
125
|
-
for sub in _SUB_PRESERVE:
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
subset[sub][key] = self._result[sub][key]
|
198
|
+
for sub, sub_keys in _SUB_PRESERVE.items():
|
199
|
+
sub_data = self._return_data.get(sub)
|
200
|
+
|
201
|
+
if isinstance(sub_data, dict):
|
202
|
+
subset[sub] = {key: value for key, value in sub_data.items() if key in sub_keys}
|
131
203
|
|
132
204
|
# DTFIX-FUTURE: is checking no_log here redundant now that we use _ansible_no_log everywhere?
|
133
|
-
if isinstance(self.
|
134
|
-
censored_result = censor_result(self.
|
205
|
+
if isinstance(self.task.no_log, bool) and self.task.no_log or self._return_data.get('_ansible_no_log'):
|
206
|
+
censored_result = censor_result(self._return_data)
|
135
207
|
|
136
|
-
if
|
208
|
+
if self._loop_results:
|
137
209
|
# maintain shape for loop results so callback behavior recognizes a loop was performed
|
138
|
-
censored_result.update(results=[
|
210
|
+
censored_result.update(results=[
|
211
|
+
censor_result(loop_res) if isinstance(loop_res, dict) and loop_res.get('_ansible_no_log') else loop_res for loop_res in self._loop_results
|
212
|
+
])
|
139
213
|
|
140
|
-
|
141
|
-
elif self.
|
142
|
-
|
143
|
-
|
144
|
-
# actually remove
|
145
|
-
for remove_key in ignore:
|
146
|
-
if remove_key in result._result:
|
147
|
-
del result._result[remove_key]
|
214
|
+
return_data = censored_result
|
215
|
+
elif self._return_data:
|
216
|
+
return_data = {k: v for k, v in module_response_deepcopy(self._return_data).items() if k not in ignore}
|
148
217
|
|
149
218
|
# remove almost ALL internal keys, keep ones relevant to callback
|
150
|
-
strip_internal_keys(
|
219
|
+
strip_internal_keys(return_data, exceptions=CLEAN_EXCEPTIONS)
|
220
|
+
else:
|
221
|
+
return_data = {}
|
151
222
|
|
152
223
|
# keep subset
|
153
|
-
|
224
|
+
return_data.update(subset)
|
225
|
+
|
226
|
+
return CallbackTaskResult(self.host, self.task, return_data, self.task_fields)
|
227
|
+
|
228
|
+
|
229
|
+
@t.final
|
230
|
+
class CallbackTaskResult(_BaseTaskResult):
|
231
|
+
"""Public contract of TaskResult """
|
232
|
+
|
233
|
+
# DTFIX-RELEASE: find a better home for this since it's public API
|
234
|
+
|
235
|
+
@property
|
236
|
+
def _result(self) -> _c.MutableMapping[str, t.Any]:
|
237
|
+
"""Use the `result` property when supporting only ansible-core 2.19 or later."""
|
238
|
+
# deprecated: description='Deprecate `_result` in favor of `result`' core_version='2.23'
|
239
|
+
return self.result
|
240
|
+
|
241
|
+
@functools.cached_property
|
242
|
+
def result(self) -> _c.MutableMapping[str, t.Any]:
|
243
|
+
"""
|
244
|
+
Returns a cached copy of the task result dictionary for consumption by callbacks.
|
245
|
+
Internal custom types are transformed to native Python types to facilitate access and serialization.
|
246
|
+
"""
|
247
|
+
return t.cast(_c.MutableMapping[str, t.Any], _vars.transform_to_native_types(self._return_data))
|
248
|
+
|
154
249
|
|
155
|
-
|
250
|
+
TaskResult = CallbackTaskResult
|
251
|
+
"""Compatibility name for the pre-2.19 callback-shaped TaskResult passed to callbacks."""
|
156
252
|
|
157
253
|
|
158
|
-
def censor_result(result:
|
254
|
+
def censor_result(result: _c.Mapping[str, t.Any]) -> dict[str, t.Any]:
|
159
255
|
censored_result = {key: value for key in _PRESERVE if (value := result.get(key, ...)) is not ...}
|
160
256
|
censored_result.update(censored="the output has been hidden due to the fact that 'no_log: true' was specified for this result")
|
161
257
|
|
ansible/inventory/manager.py
CHANGED
@@ -30,6 +30,7 @@ from random import shuffle
|
|
30
30
|
|
31
31
|
from ansible import constants as C
|
32
32
|
from ansible._internal import _json, _wrapt
|
33
|
+
from ansible._internal._json import EncryptedStringBehavior
|
33
34
|
from ansible.errors import AnsibleError, AnsibleOptionsError
|
34
35
|
from ansible.inventory.data import InventoryData
|
35
36
|
from ansible.module_utils.six import string_types
|
@@ -787,7 +788,7 @@ class _InventoryDataWrapper(_wrapt.ObjectProxy):
|
|
787
788
|
return _json.AnsibleVariableVisitor(
|
788
789
|
trusted_as_template=self._target_plugin.trusted_by_default,
|
789
790
|
origin=self._default_origin,
|
790
|
-
|
791
|
+
encrypted_string_behavior=EncryptedStringBehavior.PRESERVE,
|
791
792
|
)
|
792
793
|
|
793
794
|
def set_variable(self, entity: str, varname: str, value: t.Any) -> None:
|
ansible/module_utils/basic.py
CHANGED
@@ -53,9 +53,7 @@ try:
|
|
53
53
|
except ImportError:
|
54
54
|
HAS_SYSLOG = False
|
55
55
|
|
56
|
-
|
57
|
-
if t.TYPE_CHECKING:
|
58
|
-
from builtins import ellipsis
|
56
|
+
_UNSET = t.cast(t.Any, object())
|
59
57
|
|
60
58
|
try:
|
61
59
|
from systemd import journal, daemon as systemd_daemon
|
@@ -341,7 +339,7 @@ def _load_params():
|
|
341
339
|
except Exception as ex:
|
342
340
|
raise Exception("Failed to decode JSON module parameters.") from ex
|
343
341
|
|
344
|
-
if (ansible_module_args := params.get('ANSIBLE_MODULE_ARGS',
|
342
|
+
if (ansible_module_args := params.get('ANSIBLE_MODULE_ARGS', _UNSET)) is _UNSET:
|
345
343
|
raise Exception("ANSIBLE_MODULE_ARGS not provided.")
|
346
344
|
|
347
345
|
global _PARSED_MODULE_ARGS
|
@@ -1459,7 +1457,7 @@ class AnsibleModule(object):
|
|
1459
1457
|
self._return_formatted(kwargs)
|
1460
1458
|
sys.exit(0)
|
1461
1459
|
|
1462
|
-
def fail_json(self, msg: str, *, exception: BaseException | str |
|
1460
|
+
def fail_json(self, msg: str, *, exception: BaseException | str | None = _UNSET, **kwargs) -> t.NoReturn:
|
1463
1461
|
"""
|
1464
1462
|
Return from the module with an error message and optional exception/traceback detail.
|
1465
1463
|
A traceback will only be included in the result if error traceback capturing has been enabled.
|
@@ -1498,7 +1496,7 @@ class AnsibleModule(object):
|
|
1498
1496
|
|
1499
1497
|
if isinstance(exception, str):
|
1500
1498
|
formatted_traceback = exception
|
1501
|
-
elif exception is
|
1499
|
+
elif exception is _UNSET and (current_exception := t.cast(t.Optional[BaseException], sys.exc_info()[1])):
|
1502
1500
|
formatted_traceback = _traceback.maybe_extract_traceback(current_exception, _traceback.TracebackEvent.ERROR)
|
1503
1501
|
else:
|
1504
1502
|
formatted_traceback = _traceback.maybe_capture_traceback(_traceback.TracebackEvent.ERROR)
|
@@ -11,7 +11,7 @@ from ansible.module_utils._internal import _traceback, _plugin_exec_context
|
|
11
11
|
from ansible.module_utils.common import messages as _messages
|
12
12
|
from ansible.module_utils import _internal
|
13
13
|
|
14
|
-
_UNSET = _t.cast(_t.Any,
|
14
|
+
_UNSET = _t.cast(_t.Any, object())
|
15
15
|
|
16
16
|
|
17
17
|
def warn(warning: str) -> None:
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
2
|
+
#
|
3
|
+
# This file is part of Ansible
|
4
|
+
#
|
5
|
+
# Ansible is free software: you can redistribute it and/or modify
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
8
|
+
# (at your option) any later version.
|
9
|
+
#
|
10
|
+
# Ansible is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13
|
+
# GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License
|
16
|
+
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
import json
|
21
|
+
|
22
|
+
from ansible.utils.display import Display
|
23
|
+
|
24
|
+
Display().deprecated(f'{__name__!r} is deprecated.', version='2.23', help_text='Call `json.dumps` directly instead.')
|
25
|
+
|
26
|
+
|
27
|
+
def jsonify(result, format=False):
|
28
|
+
"""Format JSON output."""
|
29
|
+
|
30
|
+
if result is None:
|
31
|
+
return "{}"
|
32
|
+
|
33
|
+
indent = None
|
34
|
+
if format:
|
35
|
+
indent = 4
|
36
|
+
|
37
|
+
try:
|
38
|
+
return json.dumps(result, sort_keys=True, indent=indent, ensure_ascii=False)
|
39
|
+
except UnicodeDecodeError:
|
40
|
+
return json.dumps(result, sort_keys=True, indent=indent)
|
ansible/parsing/yaml/objects.py
CHANGED
@@ -8,25 +8,36 @@ from ansible.module_utils._internal import _datatag
|
|
8
8
|
from ansible.module_utils.common.text import converters as _converters
|
9
9
|
from ansible.parsing import vault as _vault
|
10
10
|
|
11
|
+
_UNSET = _t.cast(_t.Any, object())
|
12
|
+
|
11
13
|
|
12
14
|
class _AnsibleMapping(dict):
|
13
15
|
"""Backwards compatibility type."""
|
14
16
|
|
15
|
-
def __new__(cls, value):
|
16
|
-
|
17
|
+
def __new__(cls, value=_UNSET, /, **kwargs):
|
18
|
+
if value is _UNSET:
|
19
|
+
return dict(**kwargs)
|
20
|
+
|
21
|
+
return _datatag.AnsibleTagHelper.tag_copy(value, dict(value, **kwargs))
|
17
22
|
|
18
23
|
|
19
24
|
class _AnsibleUnicode(str):
|
20
25
|
"""Backwards compatibility type."""
|
21
26
|
|
22
|
-
def __new__(cls,
|
23
|
-
|
27
|
+
def __new__(cls, object=_UNSET, **kwargs):
|
28
|
+
if object is _UNSET:
|
29
|
+
return str(**kwargs)
|
30
|
+
|
31
|
+
return _datatag.AnsibleTagHelper.tag_copy(object, str(object, **kwargs))
|
24
32
|
|
25
33
|
|
26
34
|
class _AnsibleSequence(list):
|
27
35
|
"""Backwards compatibility type."""
|
28
36
|
|
29
|
-
def __new__(cls, value):
|
37
|
+
def __new__(cls, value=_UNSET, /):
|
38
|
+
if value is _UNSET:
|
39
|
+
return list()
|
40
|
+
|
30
41
|
return _datatag.AnsibleTagHelper.tag_copy(value, list(value))
|
31
42
|
|
32
43
|
|