ansible-core 2.19.0b1__py3-none-any.whl → 2.19.0b3__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/_ansiballz.py +1 -4
- 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/_datatag.py +3 -4
- ansible/_internal/_templating/_engine.py +6 -1
- ansible/_internal/_templating/_jinja_bits.py +4 -4
- ansible/_internal/_templating/_jinja_plugins.py +7 -17
- ansible/cli/__init__.py +12 -5
- ansible/cli/arguments/option_helpers.py +4 -1
- ansible/cli/doc.py +14 -8
- ansible/config/base.yml +17 -20
- ansible/config/manager.py +2 -2
- ansible/constants.py +0 -62
- ansible/errors/__init__.py +6 -2
- ansible/executor/module_common.py +11 -7
- ansible/executor/process/worker.py +31 -26
- ansible/executor/task_executor.py +38 -31
- ansible/executor/task_queue_manager.py +62 -52
- ansible/executor/task_result.py +168 -72
- ansible/galaxy/api.py +1 -1
- ansible/galaxy/collection/__init__.py +3 -3
- ansible/inventory/manager.py +2 -1
- ansible/module_utils/_internal/_ansiballz.py +4 -30
- ansible/module_utils/_internal/_datatag/_tags.py +3 -25
- ansible/module_utils/_internal/_deprecator.py +134 -0
- ansible/module_utils/_internal/_plugin_info.py +25 -0
- ansible/module_utils/_internal/_validation.py +14 -0
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +68 -23
- ansible/module_utils/common/arg_spec.py +8 -3
- ansible/module_utils/common/messages.py +40 -23
- ansible/module_utils/common/process.py +0 -1
- ansible/module_utils/common/respawn.py +0 -7
- ansible/module_utils/common/warnings.py +13 -13
- ansible/module_utils/datatag.py +13 -13
- ansible/modules/async_status.py +1 -1
- ansible/modules/dnf5.py +1 -1
- ansible/modules/get_url.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/playbook/task.py +0 -2
- ansible/plugins/__init__.py +18 -8
- ansible/plugins/action/__init__.py +6 -14
- ansible/plugins/action/gather_facts.py +2 -4
- 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 +23 -16
- ansible/plugins/callback/tree.py +13 -6
- ansible/plugins/connection/local.py +1 -1
- ansible/plugins/connection/paramiko_ssh.py +9 -2
- ansible/plugins/doc_fragments/action_core.py +1 -1
- ansible/plugins/filter/core.py +12 -2
- ansible/plugins/inventory/__init__.py +2 -2
- ansible/plugins/loader.py +194 -130
- ansible/plugins/lookup/url.py +2 -2
- ansible/plugins/strategy/__init__.py +76 -82
- 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 +8 -6
- ansible/utils/collection_loader/_collection_meta.py +5 -3
- ansible/utils/display.py +141 -79
- ansible/utils/py3compat.py +1 -7
- ansible/utils/ssh_functions.py +4 -1
- ansible/utils/vars.py +23 -0
- ansible/vars/clean.py +1 -1
- ansible/vars/manager.py +18 -27
- ansible/vars/plugins.py +4 -4
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/RECORD +89 -85
- ansible_test/_internal/commands/sanity/pylint.py +1 -0
- ansible_test/_internal/docker_util.py +4 -3
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +475 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
- ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
- ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/COPYING +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/top_level.txt +0 -0
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/galaxy/api.py
CHANGED
@@ -138,7 +138,7 @@ def g_connect(versions):
|
|
138
138
|
'The v2 Ansible Galaxy API is deprecated and no longer supported. '
|
139
139
|
'Ensure that you have configured the ansible-galaxy CLI to utilize an '
|
140
140
|
'updated and supported version of Ansible Galaxy.',
|
141
|
-
version='2.20'
|
141
|
+
version='2.20',
|
142
142
|
)
|
143
143
|
|
144
144
|
return method(self, *args, **kwargs)
|
@@ -201,9 +201,9 @@ class CollectionSignatureError(Exception):
|
|
201
201
|
|
202
202
|
# FUTURE: expose actual verify result details for a collection on this object, maybe reimplement as dataclass on py3.8+
|
203
203
|
class CollectionVerifyResult:
|
204
|
-
def __init__(self, collection_name
|
205
|
-
self.collection_name = collection_name
|
206
|
-
self.success = True
|
204
|
+
def __init__(self, collection_name: str) -> None:
|
205
|
+
self.collection_name = collection_name
|
206
|
+
self.success = True
|
207
207
|
|
208
208
|
|
209
209
|
def verify_local_collection(local_collection, remote_collection, artifacts_manager):
|
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:
|
@@ -6,7 +6,6 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import atexit
|
9
|
-
import dataclasses
|
10
9
|
import importlib.util
|
11
10
|
import json
|
12
11
|
import os
|
@@ -15,17 +14,14 @@ import sys
|
|
15
14
|
import typing as t
|
16
15
|
|
17
16
|
from . import _errors
|
18
|
-
from ._plugin_exec_context import PluginExecContext, HasPluginInfo
|
19
17
|
from .. import basic
|
20
18
|
from ..common.json import get_module_encoder, Direction
|
21
|
-
from ..common.messages import PluginInfo
|
22
19
|
|
23
20
|
|
24
21
|
def run_module(
|
25
22
|
*,
|
26
23
|
json_params: bytes,
|
27
24
|
profile: str,
|
28
|
-
plugin_info_dict: dict[str, object],
|
29
25
|
module_fqn: str,
|
30
26
|
modlib_path: str,
|
31
27
|
init_globals: dict[str, t.Any] | None = None,
|
@@ -38,7 +34,6 @@ def run_module(
|
|
38
34
|
_run_module(
|
39
35
|
json_params=json_params,
|
40
36
|
profile=profile,
|
41
|
-
plugin_info_dict=plugin_info_dict,
|
42
37
|
module_fqn=module_fqn,
|
43
38
|
modlib_path=modlib_path,
|
44
39
|
init_globals=init_globals,
|
@@ -80,7 +75,6 @@ def _run_module(
|
|
80
75
|
*,
|
81
76
|
json_params: bytes,
|
82
77
|
profile: str,
|
83
|
-
plugin_info_dict: dict[str, object],
|
84
78
|
module_fqn: str,
|
85
79
|
modlib_path: str,
|
86
80
|
init_globals: dict[str, t.Any] | None = None,
|
@@ -92,12 +86,11 @@ def _run_module(
|
|
92
86
|
init_globals = init_globals or {}
|
93
87
|
init_globals.update(_module_fqn=module_fqn, _modlib_path=modlib_path)
|
94
88
|
|
95
|
-
|
96
|
-
|
97
|
-
runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
|
89
|
+
# Run the module. By importing it as '__main__', it executes as a script.
|
90
|
+
runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
|
98
91
|
|
99
|
-
|
100
|
-
|
92
|
+
# An Ansible module must print its own results and exit. If execution reaches this point, that did not happen.
|
93
|
+
raise RuntimeError('New-style module did not handle its own exit.')
|
101
94
|
|
102
95
|
|
103
96
|
def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
|
@@ -112,22 +105,3 @@ def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
|
|
112
105
|
print(json.dumps(result, cls=encoder)) # pylint: disable=ansible-bad-function
|
113
106
|
|
114
107
|
sys.exit(1) # pylint: disable=ansible-bad-function
|
115
|
-
|
116
|
-
|
117
|
-
@dataclasses.dataclass(frozen=True)
|
118
|
-
class _ModulePluginWrapper(HasPluginInfo):
|
119
|
-
"""Modules aren't plugin instances; this adapter implements the `HasPluginInfo` protocol to allow `PluginExecContext` infra to work with modules."""
|
120
|
-
|
121
|
-
plugin: PluginInfo
|
122
|
-
|
123
|
-
@property
|
124
|
-
def _load_name(self) -> str:
|
125
|
-
return self.plugin.requested_name
|
126
|
-
|
127
|
-
@property
|
128
|
-
def ansible_name(self) -> str:
|
129
|
-
return self.plugin.resolved_name
|
130
|
-
|
131
|
-
@property
|
132
|
-
def plugin_type(self) -> str:
|
133
|
-
return self.plugin.type
|
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import dataclasses
|
4
|
-
import datetime
|
5
4
|
import typing as t
|
6
5
|
|
7
6
|
from ansible.module_utils.common import messages as _messages
|
@@ -12,27 +11,6 @@ from ansible.module_utils._internal import _datatag
|
|
12
11
|
class Deprecated(_datatag.AnsibleDatatagBase):
|
13
12
|
msg: str
|
14
13
|
help_text: t.Optional[str] = None
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@classmethod
|
20
|
-
def _from_dict(cls, d: t.Dict[str, t.Any]) -> Deprecated:
|
21
|
-
source = d
|
22
|
-
removal_date = source.get('removal_date')
|
23
|
-
|
24
|
-
if removal_date is not None:
|
25
|
-
source = source.copy()
|
26
|
-
source['removal_date'] = datetime.date.fromisoformat(removal_date)
|
27
|
-
|
28
|
-
return cls(**source)
|
29
|
-
|
30
|
-
def _as_dict(self) -> t.Dict[str, t.Any]:
|
31
|
-
# deprecated: description='no-args super() with slotted dataclass requires 3.14+' python_version='3.13'
|
32
|
-
# see: https://github.com/python/cpython/pull/124455
|
33
|
-
value = super(Deprecated, self)._as_dict()
|
34
|
-
|
35
|
-
if self.removal_date is not None:
|
36
|
-
value['removal_date'] = self.removal_date.isoformat()
|
37
|
-
|
38
|
-
return value
|
14
|
+
date: t.Optional[str] = None
|
15
|
+
version: t.Optional[str] = None
|
16
|
+
deprecator: t.Optional[_messages.PluginInfo] = None
|
@@ -0,0 +1,134 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
import re
|
5
|
+
import pathlib
|
6
|
+
import sys
|
7
|
+
import typing as t
|
8
|
+
|
9
|
+
from ansible.module_utils.common.messages import PluginInfo
|
10
|
+
|
11
|
+
_ansible_module_base_path: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
12
|
+
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
13
|
+
|
14
|
+
ANSIBLE_CORE_DEPRECATOR: t.Final = PluginInfo._from_collection_name('ansible.builtin')
|
15
|
+
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
|
16
|
+
|
17
|
+
INDETERMINATE_DEPRECATOR: t.Final = PluginInfo(resolved_name='indeterminate', type='indeterminate')
|
18
|
+
"""Singleton `PluginInfo` instance for indeterminate deprecator."""
|
19
|
+
|
20
|
+
_DEPRECATOR_PLUGIN_TYPES = frozenset(
|
21
|
+
{
|
22
|
+
'action',
|
23
|
+
'become',
|
24
|
+
'cache',
|
25
|
+
'callback',
|
26
|
+
'cliconf',
|
27
|
+
'connection',
|
28
|
+
# doc_fragments - no code execution
|
29
|
+
# filter - basename inadequate to identify plugin
|
30
|
+
'httpapi',
|
31
|
+
'inventory',
|
32
|
+
'lookup',
|
33
|
+
'module', # only for collections
|
34
|
+
'netconf',
|
35
|
+
'shell',
|
36
|
+
'strategy',
|
37
|
+
'terminal',
|
38
|
+
# test - basename inadequate to identify plugin
|
39
|
+
'vars',
|
40
|
+
}
|
41
|
+
)
|
42
|
+
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
43
|
+
|
44
|
+
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES = frozenset(
|
45
|
+
{
|
46
|
+
'filter',
|
47
|
+
'test',
|
48
|
+
}
|
49
|
+
)
|
50
|
+
"""Plugin types for which basename cannot be used to identify the plugin name."""
|
51
|
+
|
52
|
+
|
53
|
+
def get_best_deprecator(*, deprecator: PluginInfo | None = None, collection_name: str | None = None) -> PluginInfo:
|
54
|
+
"""Return the best-available `PluginInfo` for the caller of this method."""
|
55
|
+
_skip_stackwalk = True
|
56
|
+
|
57
|
+
if deprecator and collection_name:
|
58
|
+
raise ValueError('Specify only one of `deprecator` or `collection_name`.')
|
59
|
+
|
60
|
+
return deprecator or PluginInfo._from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
|
61
|
+
|
62
|
+
|
63
|
+
def get_caller_plugin_info() -> PluginInfo | None:
|
64
|
+
"""Try to get `PluginInfo` for the caller of this method, ignoring marked infrastructure stack frames."""
|
65
|
+
_skip_stackwalk = True
|
66
|
+
|
67
|
+
if frame_info := next((frame_info for frame_info in inspect.stack() if '_skip_stackwalk' not in frame_info.frame.f_locals), None):
|
68
|
+
return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
|
69
|
+
|
70
|
+
return None # pragma: nocover
|
71
|
+
|
72
|
+
|
73
|
+
def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
74
|
+
"""Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
|
75
|
+
try:
|
76
|
+
relpath = str(pathlib.Path(path).relative_to(_ansible_module_base_path))
|
77
|
+
except ValueError:
|
78
|
+
return None # not ansible-core
|
79
|
+
|
80
|
+
namespace = 'ansible.builtin'
|
81
|
+
|
82
|
+
if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
|
83
|
+
plugin_name = match.group("plugin_name")
|
84
|
+
plugin_type = match.group("plugin_type")
|
85
|
+
|
86
|
+
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
87
|
+
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
88
|
+
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
|
89
|
+
# Callers in this case need to identify the deprecating plugin name, otherwise only ansible-core will be reported.
|
90
|
+
# Reporting ansible-core is never wrong, it just may be missing an additional detail (plugin name) in the "on behalf of" case.
|
91
|
+
return ANSIBLE_CORE_DEPRECATOR
|
92
|
+
elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
|
93
|
+
# AnsiballZ Python package for core modules
|
94
|
+
plugin_name = match.group("module_name")
|
95
|
+
plugin_type = "module"
|
96
|
+
elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
|
97
|
+
# AnsiballZ Python package for non-core library/role modules
|
98
|
+
namespace = 'ansible.legacy'
|
99
|
+
|
100
|
+
plugin_name = match.group("module_name")
|
101
|
+
plugin_type = "module"
|
102
|
+
else:
|
103
|
+
return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above
|
104
|
+
|
105
|
+
name = f'{namespace}.{plugin_name}'
|
106
|
+
|
107
|
+
return PluginInfo(resolved_name=name, type=plugin_type)
|
108
|
+
|
109
|
+
|
110
|
+
def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
111
|
+
"""Return a `PluginInfo` instance if the provided `path` refers to a collection plugin."""
|
112
|
+
if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
|
113
|
+
return None
|
114
|
+
|
115
|
+
plugin_type = match.group('plugin_type')
|
116
|
+
|
117
|
+
if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
|
118
|
+
# We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
|
119
|
+
# To keep things simple we'll fall back to just identifying the namespace and collection.
|
120
|
+
# In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
|
121
|
+
return PluginInfo._from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
|
122
|
+
|
123
|
+
if plugin_type == 'modules':
|
124
|
+
plugin_type = 'module'
|
125
|
+
|
126
|
+
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
127
|
+
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
128
|
+
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
|
129
|
+
# Callers in this case need to identify the deprecator to avoid ambiguity, since it could be the same collection or another collection.
|
130
|
+
return INDETERMINATE_DEPRECATOR
|
131
|
+
|
132
|
+
name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
|
133
|
+
|
134
|
+
return PluginInfo(resolved_name=name, type=plugin_type)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import typing as t
|
4
|
+
|
5
|
+
from ..common import messages as _messages
|
6
|
+
|
7
|
+
|
8
|
+
class HasPluginInfo(t.Protocol):
|
9
|
+
"""Protocol to type-annotate and expose PluginLoader-set values."""
|
10
|
+
|
11
|
+
@property
|
12
|
+
def ansible_name(self) -> str | None:
|
13
|
+
"""Fully resolved plugin name."""
|
14
|
+
|
15
|
+
@property
|
16
|
+
def plugin_type(self) -> str:
|
17
|
+
"""Plugin type name."""
|
18
|
+
|
19
|
+
|
20
|
+
def get_plugin_info(value: HasPluginInfo) -> _messages.PluginInfo:
|
21
|
+
"""Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol."""
|
22
|
+
return _messages.PluginInfo(
|
23
|
+
resolved_name=value.ansible_name,
|
24
|
+
type=value.plugin_type,
|
25
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import keyword
|
4
|
+
|
5
|
+
|
6
|
+
def validate_collection_name(collection_name: object, name: str = 'collection_name') -> None:
|
7
|
+
"""Validate a collection name."""
|
8
|
+
if not isinstance(collection_name, str):
|
9
|
+
raise TypeError(f"{name} must be {str} instead of {type(collection_name)}")
|
10
|
+
|
11
|
+
parts = collection_name.split('.')
|
12
|
+
|
13
|
+
if len(parts) != 2 or not all(part.isidentifier() and not keyword.iskeyword(part) for part in parts):
|
14
|
+
raise ValueError(f"{name} must consist of two non-keyword identifiers separated by '.'")
|