ansible-core 2.19.0b3__py3-none-any.whl → 2.19.0b5__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/__init__.py +2 -2
- ansible/_internal/_collection_proxy.py +1 -1
- ansible/_internal/_errors/_alarm_timeout.py +66 -0
- ansible/_internal/_errors/_captured.py +25 -30
- ansible/_internal/_errors/_error_factory.py +89 -0
- ansible/_internal/_errors/_error_utils.py +240 -0
- ansible/_internal/_errors/_task_timeout.py +28 -0
- ansible/_internal/_event_formatting.py +127 -0
- ansible/_internal/_json/__init__.py +6 -6
- ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
- ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
- ansible/_internal/_json/_profiles/_legacy.py +3 -11
- ansible/_internal/_ssh/__init__.py +0 -0
- ansible/_internal/_ssh/_agent_launch.py +91 -0
- ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
- ansible/_internal/_templating/__init__.py +5 -3
- ansible/_internal/_templating/_datatag.py +2 -1
- ansible/_internal/_templating/_engine.py +3 -4
- ansible/_internal/_templating/_jinja_bits.py +21 -16
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +31 -3
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_transform.py +20 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_testing.py +26 -0
- ansible/_internal/_yaml/_dumper.py +1 -1
- ansible/_internal/_yaml/_errors.py +7 -7
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
- ansible/cli/__init__.py +5 -82
- ansible/cli/arguments/option_helpers.py +8 -5
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +27 -23
- ansible/config/manager.py +142 -101
- ansible/constants.py +1 -1
- ansible/errors/__init__.py +96 -49
- ansible/executor/module_common.py +8 -10
- ansible/executor/powershell/async_watchdog.ps1 +2 -2
- ansible/executor/powershell/async_wrapper.ps1 +3 -3
- ansible/executor/powershell/become_wrapper.ps1 +20 -2
- ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
- ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
- ansible/executor/powershell/exec_wrapper.ps1 +219 -6
- ansible/executor/powershell/module_manifest.py +52 -0
- ansible/executor/powershell/module_wrapper.ps1 +47 -21
- ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
- ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
- ansible/executor/process/worker.py +38 -113
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/collection/__init__.py +1 -4
- ansible/inventory/manager.py +1 -0
- ansible/module_utils/_internal/__init__.py +0 -3
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz.py +4 -2
- ansible/module_utils/_internal/_datatag/__init__.py +20 -14
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +66 -48
- ansible/module_utils/_internal/_errors.py +88 -17
- ansible/module_utils/_internal/_event_utils.py +61 -0
- ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
- ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +1 -1
- ansible/module_utils/_internal/_stack.py +22 -0
- ansible/module_utils/_internal/_text_utils.py +6 -0
- ansible/module_utils/_internal/_traceback.py +11 -8
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +49 -15
- ansible/module_utils/common/arg_spec.py +2 -2
- ansible/module_utils/common/collections.py +6 -0
- ansible/module_utils/common/json.py +2 -2
- ansible/module_utils/common/text/converters.py +3 -3
- ansible/module_utils/common/validation.py +1 -1
- ansible/module_utils/common/warnings.py +80 -23
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +15 -22
- ansible/modules/git.py +1 -6
- ansible/modules/hostname.py +0 -1
- ansible/modules/pip.py +2 -4
- ansible/modules/service.py +3 -9
- ansible/modules/sysvinit.py +3 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +4 -4
- ansible/parsing/mod_args.py +1 -1
- ansible/parsing/plugin_docs.py +2 -2
- ansible/parsing/utils/yaml.py +3 -3
- ansible/parsing/vault/__init__.py +4 -4
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +0 -3
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +9 -32
- ansible/plugins/action/add_host.py +1 -1
- ansible/plugins/action/assemble.py +8 -16
- ansible/plugins/action/async_status.py +7 -2
- ansible/plugins/action/copy.py +8 -7
- ansible/plugins/action/gather_facts.py +8 -8
- ansible/plugins/action/package.py +5 -8
- ansible/plugins/action/script.py +8 -15
- ansible/plugins/action/service.py +3 -7
- ansible/plugins/action/template.py +6 -8
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/connection/local.py +2 -2
- ansible/plugins/connection/ssh.py +17 -9
- ansible/plugins/connection/winrm.py +5 -2
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +13 -6
- ansible/plugins/filter/encryption.py +4 -4
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +69 -16
- ansible/plugins/loader.py +10 -9
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +66 -9
- ansible/plugins/shell/sh.py +9 -5
- ansible/plugins/test/core.py +21 -15
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +30 -2
- ansible/utils/collection_loader/__init__.py +2 -0
- ansible/utils/display.py +107 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/docker.txt +3 -3
- ansible_test/_data/completion/remote.txt +1 -0
- ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/util.py +20 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
ansible/config/manager.py
CHANGED
@@ -16,11 +16,12 @@ import typing as t
|
|
16
16
|
from collections.abc import Mapping, Sequence
|
17
17
|
from jinja2.nativetypes import NativeEnvironment
|
18
18
|
|
19
|
+
from ansible._internal._datatag import _tags
|
19
20
|
from ansible.errors import AnsibleOptionsError, AnsibleError, AnsibleUndefinedConfigEntry, AnsibleRequiredOptionError
|
21
|
+
from ansible.module_utils._internal._datatag import AnsibleTagHelper
|
20
22
|
from ansible.module_utils.common.sentinel import Sentinel
|
21
23
|
from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
|
22
24
|
from ansible.module_utils.common.yaml import yaml_load
|
23
|
-
from ansible.module_utils.six import string_types
|
24
25
|
from ansible.module_utils.parsing.convert_bool import boolean
|
25
26
|
from ansible.parsing.quoting import unquote
|
26
27
|
from ansible.utils.path import cleanup_tmp_file, makedirs_safe, unfrackpath
|
@@ -50,6 +51,14 @@ GALAXY_SERVER_ADDITIONAL = {
|
|
50
51
|
}
|
51
52
|
|
52
53
|
|
54
|
+
@t.runtime_checkable
|
55
|
+
class _EncryptedStringProtocol(t.Protocol):
|
56
|
+
"""Protocol representing an `EncryptedString`, since it cannot be imported here."""
|
57
|
+
# DTFIX-FUTURE: collapse this with the one in collection loader, once we can
|
58
|
+
|
59
|
+
def _decrypt(self) -> str: ...
|
60
|
+
|
61
|
+
|
53
62
|
def _get_config_label(plugin_type: str, plugin_name: str, config: str) -> str:
|
54
63
|
"""Return a label for the given config."""
|
55
64
|
entry = f'{config!r}'
|
@@ -65,133 +74,157 @@ def _get_config_label(plugin_type: str, plugin_name: str, config: str) -> str:
|
|
65
74
|
return entry
|
66
75
|
|
67
76
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
:string: Same as 'str'
|
77
|
+
def ensure_type(value: object, value_type: str | None, origin: str | None = None, origin_ftype: str | None = None) -> t.Any:
|
78
|
+
"""
|
79
|
+
Converts `value` to the requested `value_type`; raises `ValueError` for failed conversions.
|
80
|
+
|
81
|
+
Values for `value_type` are:
|
82
|
+
|
83
|
+
* boolean/bool: Return a `bool` by applying non-strict `bool` filter rules:
|
84
|
+
'y', 'yes', 'on', '1', 'true', 't', 1, 1.0, True return True, any other value is False.
|
85
|
+
* integer/int: Return an `int`. Accepts any `str` parseable by `int` or numeric value with a zero mantissa (including `bool`).
|
86
|
+
* float: Return a `float`. Accepts any `str` parseable by `float` or numeric value (including `bool`).
|
87
|
+
* list: Return a `list`. Accepts `list` or `Sequence`. Also accepts, `str`, splitting on ',' while stripping whitespace and unquoting items.
|
88
|
+
* none: Return `None`. Accepts only the string "None".
|
89
|
+
* path: Return a resolved path. Accepts `str`.
|
90
|
+
* temppath/tmppath/tmp: Return a unique temporary directory inside the resolved path specified by the value.
|
91
|
+
* pathspec: Return a `list` of resolved paths. Accepts a `list` or `Sequence`. Also accepts `str`, splitting on ':'.
|
92
|
+
* pathlist: Return a `list` of resolved paths. Accepts a `list` or `Sequence`. Also accepts `str`, splitting on `,` while stripping whitespace from paths.
|
93
|
+
* dictionary/dict: Return a `dict`. Accepts `dict` or `Mapping`.
|
94
|
+
* string/str: Return a `str`. Accepts `bool`, `int`, `float`, `complex` or `str`.
|
95
|
+
|
96
|
+
Path resolution ensures paths are `str` with expansion of '{{CWD}}', environment variables and '~'.
|
97
|
+
Non-absolute paths are expanded relative to the basedir from `origin`, if specified.
|
98
|
+
|
99
|
+
No conversion is performed if `value_type` is unknown or `value` is `None`.
|
100
|
+
When `origin_ftype` is "ini", a `str` result will be unquoted.
|
93
101
|
"""
|
94
102
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
103
|
+
if value is None:
|
104
|
+
return None
|
105
|
+
|
106
|
+
original_value = value
|
107
|
+
copy_tags = value_type not in ('temppath', 'tmppath', 'tmp')
|
108
|
+
|
109
|
+
value = _ensure_type(value, value_type, origin)
|
110
|
+
|
111
|
+
if copy_tags and value is not original_value:
|
112
|
+
if isinstance(value, list):
|
113
|
+
value = [AnsibleTagHelper.tag_copy(original_value, item) for item in value]
|
114
|
+
|
115
|
+
value = AnsibleTagHelper.tag_copy(original_value, value)
|
116
|
+
|
117
|
+
if isinstance(value, str) and origin_ftype and origin_ftype == 'ini':
|
118
|
+
value = unquote(value)
|
119
|
+
|
120
|
+
return value
|
121
|
+
|
122
|
+
|
123
|
+
def _ensure_type(value: object, value_type: str | None, origin: str | None = None) -> t.Any:
|
124
|
+
"""Internal implementation for `ensure_type`, call that function instead."""
|
125
|
+
original_value = value
|
126
|
+
basedir = origin if origin and os.path.isabs(origin) and os.path.exists(to_bytes(origin)) else None
|
99
127
|
|
100
128
|
if value_type:
|
101
129
|
value_type = value_type.lower()
|
102
130
|
|
103
|
-
|
104
|
-
|
105
|
-
|
131
|
+
match value_type:
|
132
|
+
case 'boolean' | 'bool':
|
133
|
+
return boolean(value, strict=False)
|
134
|
+
|
135
|
+
case 'integer' | 'int':
|
136
|
+
if isinstance(value, int): # handle both int and bool (which is an int)
|
137
|
+
return int(value)
|
106
138
|
|
107
|
-
|
108
|
-
if not isinstance(value, int):
|
139
|
+
if isinstance(value, (float, str)):
|
109
140
|
try:
|
141
|
+
# use Decimal for all other source type conversions; non-zero mantissa is a failure
|
110
142
|
if (decimal_value := decimal.Decimal(value)) == (int_part := int(decimal_value)):
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
143
|
+
return int_part
|
144
|
+
except (decimal.DecimalException, ValueError):
|
145
|
+
pass
|
146
|
+
|
147
|
+
case 'float':
|
148
|
+
if isinstance(value, float):
|
149
|
+
return value
|
116
150
|
|
117
|
-
|
118
|
-
|
119
|
-
|
151
|
+
if isinstance(value, (int, str)):
|
152
|
+
try:
|
153
|
+
return float(value)
|
154
|
+
except ValueError:
|
155
|
+
pass
|
156
|
+
|
157
|
+
case 'list':
|
158
|
+
if isinstance(value, list):
|
159
|
+
return value
|
120
160
|
|
121
|
-
|
122
|
-
|
123
|
-
value = [unquote(x.strip()) for x in value.split(',')]
|
124
|
-
elif not isinstance(value, Sequence):
|
125
|
-
errmsg = 'list'
|
161
|
+
if isinstance(value, str):
|
162
|
+
return [unquote(x.strip()) for x in value.split(',')]
|
126
163
|
|
127
|
-
|
164
|
+
if isinstance(value, Sequence) and not isinstance(value, bytes):
|
165
|
+
return list(value)
|
166
|
+
|
167
|
+
case 'none':
|
128
168
|
if value == "None":
|
129
|
-
|
169
|
+
return None
|
130
170
|
|
131
|
-
|
132
|
-
|
171
|
+
case 'path':
|
172
|
+
if isinstance(value, str):
|
173
|
+
return resolve_path(value, basedir=basedir)
|
133
174
|
|
134
|
-
|
135
|
-
if isinstance(value,
|
175
|
+
case 'temppath' | 'tmppath' | 'tmp':
|
176
|
+
if isinstance(value, str):
|
136
177
|
value = resolve_path(value, basedir=basedir)
|
137
|
-
else:
|
138
|
-
errmsg = 'path'
|
139
178
|
|
140
|
-
elif value_type in ('tmp', 'temppath', 'tmppath'):
|
141
|
-
if isinstance(value, string_types):
|
142
|
-
value = resolve_path(value, basedir=basedir)
|
143
179
|
if not os.path.exists(value):
|
144
180
|
makedirs_safe(value, 0o700)
|
181
|
+
|
145
182
|
prefix = 'ansible-local-%s' % os.getpid()
|
146
183
|
value = tempfile.mkdtemp(prefix=prefix, dir=value)
|
147
184
|
atexit.register(cleanup_tmp_file, value, warn=True)
|
148
|
-
else:
|
149
|
-
errmsg = 'temppath'
|
150
185
|
|
151
|
-
|
152
|
-
|
186
|
+
return value
|
187
|
+
|
188
|
+
case 'pathspec':
|
189
|
+
if isinstance(value, str):
|
153
190
|
value = value.split(os.pathsep)
|
154
191
|
|
155
|
-
if isinstance(value, Sequence):
|
156
|
-
|
157
|
-
else:
|
158
|
-
errmsg = 'pathspec'
|
192
|
+
if isinstance(value, Sequence) and not isinstance(value, bytes) and all(isinstance(x, str) for x in value):
|
193
|
+
return [resolve_path(x, basedir=basedir) for x in value]
|
159
194
|
|
160
|
-
|
161
|
-
if isinstance(value,
|
195
|
+
case 'pathlist':
|
196
|
+
if isinstance(value, str):
|
162
197
|
value = [x.strip() for x in value.split(',')]
|
163
198
|
|
164
|
-
if isinstance(value, Sequence):
|
165
|
-
|
166
|
-
else:
|
167
|
-
errmsg = 'pathlist'
|
199
|
+
if isinstance(value, Sequence) and not isinstance(value, bytes) and all(isinstance(x, str) for x in value):
|
200
|
+
return [resolve_path(x, basedir=basedir) for x in value]
|
168
201
|
|
169
|
-
|
170
|
-
if
|
171
|
-
|
202
|
+
case 'dictionary' | 'dict':
|
203
|
+
if isinstance(value, dict):
|
204
|
+
return value
|
172
205
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
errmsg = 'string'
|
206
|
+
if isinstance(value, Mapping):
|
207
|
+
return dict(value)
|
208
|
+
|
209
|
+
case 'string' | 'str':
|
210
|
+
if isinstance(value, str):
|
211
|
+
return value
|
180
212
|
|
181
|
-
|
182
|
-
|
183
|
-
value = to_text(value, errors='surrogate_or_strict')
|
184
|
-
if origin_ftype and origin_ftype == 'ini':
|
185
|
-
value = unquote(value)
|
213
|
+
if isinstance(value, (bool, int, float, complex)):
|
214
|
+
return str(value)
|
186
215
|
|
187
|
-
|
188
|
-
|
216
|
+
if isinstance(value, _EncryptedStringProtocol):
|
217
|
+
return value._decrypt()
|
189
218
|
|
190
|
-
|
219
|
+
case _:
|
220
|
+
# FIXME: define and document a pass-through value_type (None, 'raw', 'object', '', ...) and then deprecate acceptance of unknown types
|
221
|
+
return value # return non-str values of unknown value_type as-is
|
222
|
+
|
223
|
+
raise ValueError(f'Invalid value provided for {value_type!r}: {original_value!r}')
|
191
224
|
|
192
225
|
|
193
226
|
# FIXME: see if this can live in utils/path
|
194
|
-
def resolve_path(path, basedir=None):
|
227
|
+
def resolve_path(path: str, basedir: str | None = None) -> str:
|
195
228
|
""" resolve relative or 'variable' paths """
|
196
229
|
if '{{CWD}}' in path: # allow users to force CWD using 'magic' {{CWD}}
|
197
230
|
path = path.replace('{{CWD}}', os.getcwd())
|
@@ -304,11 +337,13 @@ def _add_base_defs_deprecations(base_defs):
|
|
304
337
|
process(entry)
|
305
338
|
|
306
339
|
|
307
|
-
class ConfigManager
|
340
|
+
class ConfigManager:
|
308
341
|
|
309
342
|
DEPRECATED = [] # type: list[tuple[str, dict[str, str]]]
|
310
343
|
WARNINGS = set() # type: set[str]
|
311
344
|
|
345
|
+
_errors: list[tuple[str, Exception]]
|
346
|
+
|
312
347
|
def __init__(self, conf_file=None, defs_file=None):
|
313
348
|
|
314
349
|
self._base_defs = {}
|
@@ -329,6 +364,9 @@ class ConfigManager(object):
|
|
329
364
|
# initialize parser and read config
|
330
365
|
self._parse_config_file()
|
331
366
|
|
367
|
+
self._errors = []
|
368
|
+
"""Deferred errors that will be turned into warnings."""
|
369
|
+
|
332
370
|
# ensure we always have config def entry
|
333
371
|
self._base_defs['CONFIG_FILE'] = {'default': None, 'type': 'path'}
|
334
372
|
|
@@ -368,16 +406,16 @@ class ConfigManager(object):
|
|
368
406
|
defs = dict((k, server_config_def(server_key, k, req, value_type)) for k, req, value_type in GALAXY_SERVER_DEF)
|
369
407
|
self.initialize_plugin_configuration_definitions('galaxy_server', server_key, defs)
|
370
408
|
|
371
|
-
def template_default(self, value, variables):
|
372
|
-
if isinstance(value,
|
409
|
+
def template_default(self, value, variables, key_name: str = '<unknown>'):
|
410
|
+
if isinstance(value, str) and (value.startswith('{{') and value.endswith('}}')) and variables is not None:
|
373
411
|
# template default values if possible
|
374
412
|
# NOTE: cannot use is_template due to circular dep
|
375
413
|
try:
|
376
414
|
# FIXME: This really should be using an immutable sandboxed native environment, not just native environment
|
377
|
-
|
378
|
-
value =
|
379
|
-
except Exception:
|
380
|
-
|
415
|
+
template = NativeEnvironment().from_string(value)
|
416
|
+
value = template.render(variables)
|
417
|
+
except Exception as ex:
|
418
|
+
self._errors.append((f'Failed to template default for config {key_name}.', ex))
|
381
419
|
return value
|
382
420
|
|
383
421
|
def _read_config_yaml_file(self, yml_file):
|
@@ -631,7 +669,7 @@ class ConfigManager(object):
|
|
631
669
|
raise AnsibleRequiredOptionError(f"Required config {_get_config_label(plugin_type, plugin_name, config)} not provided.")
|
632
670
|
else:
|
633
671
|
origin = 'default'
|
634
|
-
value = self.template_default(defs[config].get('default'), variables)
|
672
|
+
value = self.template_default(defs[config].get('default'), variables, key_name=_get_config_label(plugin_type, plugin_name, config))
|
635
673
|
|
636
674
|
try:
|
637
675
|
# ensure correct type, can raise exceptions on mismatched types
|
@@ -658,7 +696,7 @@ class ConfigManager(object):
|
|
658
696
|
|
659
697
|
if isinstance(defs[config]['choices'], Mapping):
|
660
698
|
valid = ', '.join([to_text(k) for k in defs[config]['choices'].keys()])
|
661
|
-
elif isinstance(defs[config]['choices'],
|
699
|
+
elif isinstance(defs[config]['choices'], str):
|
662
700
|
valid = defs[config]['choices']
|
663
701
|
elif isinstance(defs[config]['choices'], Sequence):
|
664
702
|
valid = ', '.join([to_text(c) for c in defs[config]['choices']])
|
@@ -674,6 +712,9 @@ class ConfigManager(object):
|
|
674
712
|
else:
|
675
713
|
raise AnsibleUndefinedConfigEntry(f'No config definition exists for {_get_config_label(plugin_type, plugin_name, config)}.')
|
676
714
|
|
715
|
+
if not _tags.Origin.is_tagged_on(value):
|
716
|
+
value = _tags.Origin(description=f'<Config {origin}>').tag(value)
|
717
|
+
|
677
718
|
return value, origin
|
678
719
|
|
679
720
|
def initialize_plugin_configuration_definitions(self, plugin_type, name, defs):
|
ansible/constants.py
CHANGED
@@ -60,7 +60,7 @@ COLOR_CODES = {
|
|
60
60
|
'magenta': u'0;35', 'bright magenta': u'1;35',
|
61
61
|
'normal': u'0',
|
62
62
|
}
|
63
|
-
REJECT_EXTS =
|
63
|
+
REJECT_EXTS = ['.pyc', '.pyo', '.swp', '.bak', '~', '.rpm', '.md', '.txt', '.rst'] # this is concatenated with other config settings as lists; cannot be tuple
|
64
64
|
BOOL_TRUE = BOOLEANS_TRUE
|
65
65
|
COLLECTION_PTYPE_COMPAT = {'module': 'modules'}
|
66
66
|
|
ansible/errors/__init__.py
CHANGED
@@ -3,20 +3,18 @@
|
|
3
3
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
|
+
import collections.abc as _c
|
6
7
|
import enum
|
7
|
-
import traceback
|
8
|
-
import sys
|
9
8
|
import types
|
10
9
|
import typing as t
|
11
10
|
|
12
|
-
from collections.abc import Sequence
|
13
|
-
|
14
11
|
from json import JSONDecodeError
|
15
12
|
|
16
13
|
from ansible.module_utils.common.text.converters import to_text
|
17
14
|
from ..module_utils.datatag import native_type_name
|
18
15
|
from ansible._internal._datatag import _tags
|
19
|
-
from .._internal._errors import
|
16
|
+
from .._internal._errors import _error_utils
|
17
|
+
from ansible.module_utils._internal import _text_utils
|
20
18
|
|
21
19
|
if t.TYPE_CHECKING:
|
22
20
|
from ansible.plugins import loader as _t_loader
|
@@ -73,7 +71,7 @@ class AnsibleError(Exception):
|
|
73
71
|
message = str(message)
|
74
72
|
|
75
73
|
if self._default_message and message:
|
76
|
-
message =
|
74
|
+
message = _text_utils.concat_message(self._default_message, message)
|
77
75
|
elif self._default_message:
|
78
76
|
message = self._default_message
|
79
77
|
elif not message:
|
@@ -108,12 +106,10 @@ class AnsibleError(Exception):
|
|
108
106
|
@property
|
109
107
|
def message(self) -> str:
|
110
108
|
"""
|
111
|
-
|
112
|
-
|
113
|
-
The recursion is due to `AnsibleError.__str__` calling this method, which uses `str` on child exceptions to create the cause message.
|
114
|
-
Recursion stops on the first non-AnsibleError since those exceptions do not implement the custom `__str__` behavior.
|
109
|
+
Return the original message with cause message(s) appended.
|
110
|
+
The cause will not be followed on any `AnsibleError` with `_include_cause_message=False`.
|
115
111
|
"""
|
116
|
-
return
|
112
|
+
return _error_utils.format_exception_message(self)
|
117
113
|
|
118
114
|
@message.setter
|
119
115
|
def message(self, val) -> None:
|
@@ -121,8 +117,8 @@ class AnsibleError(Exception):
|
|
121
117
|
|
122
118
|
@property
|
123
119
|
def _formatted_source_context(self) -> str | None:
|
124
|
-
with
|
125
|
-
if source_context :=
|
120
|
+
with _error_utils.RedactAnnotatedSourceContext.when(not self._show_content):
|
121
|
+
if source_context := _error_utils.SourceContext.from_value(self.obj):
|
126
122
|
return str(source_context)
|
127
123
|
|
128
124
|
return None
|
@@ -238,8 +234,20 @@ class AnsibleModuleError(AnsibleRuntimeError):
|
|
238
234
|
"""A module failed somehow."""
|
239
235
|
|
240
236
|
|
241
|
-
class AnsibleConnectionFailure(AnsibleRuntimeError):
|
242
|
-
"""
|
237
|
+
class AnsibleConnectionFailure(AnsibleRuntimeError, _error_utils.ContributesToTaskResult):
|
238
|
+
"""
|
239
|
+
The transport / connection_plugin had a fatal error.
|
240
|
+
|
241
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
242
|
+
"""
|
243
|
+
|
244
|
+
@property
|
245
|
+
def result_contribution(self) -> t.Mapping[str, object]:
|
246
|
+
return dict(unreachable=True)
|
247
|
+
|
248
|
+
@property
|
249
|
+
def omit_failed_key(self) -> bool:
|
250
|
+
return True
|
243
251
|
|
244
252
|
|
245
253
|
class AnsibleAuthenticationFailure(AnsibleConnectionFailure):
|
@@ -319,7 +327,7 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
|
|
319
327
|
else:
|
320
328
|
message += "Could not find file"
|
321
329
|
|
322
|
-
if self.paths and isinstance(self.paths, Sequence):
|
330
|
+
if self.paths and isinstance(self.paths, _c.Sequence):
|
323
331
|
searched = to_text('\n\t'.join(self.paths))
|
324
332
|
if message:
|
325
333
|
message += "\n"
|
@@ -331,47 +339,76 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
|
|
331
339
|
suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
|
332
340
|
|
333
341
|
|
334
|
-
|
335
|
-
# DO NOT USE as they will probably be removed soon.
|
336
|
-
# We will port the action modules in our tree to use a context manager instead.
|
337
|
-
class AnsibleAction(AnsibleRuntimeError):
|
342
|
+
class AnsibleAction(AnsibleRuntimeError, _error_utils.ContributesToTaskResult):
|
338
343
|
"""Base Exception for Action plugin flow control."""
|
339
344
|
|
340
345
|
def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
|
341
|
-
super(
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
346
|
+
super().__init__(message=message, obj=obj, show_content=show_content, suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
|
347
|
+
|
348
|
+
self._result = result or {}
|
349
|
+
|
350
|
+
@property
|
351
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
352
|
+
return self._result
|
353
|
+
|
354
|
+
@property
|
355
|
+
def result(self) -> dict[str, object]:
|
356
|
+
"""Backward compatibility property returning a mutable dictionary."""
|
357
|
+
return dict(self.result_contribution)
|
347
358
|
|
348
359
|
|
349
360
|
class AnsibleActionSkip(AnsibleAction):
|
350
|
-
"""
|
361
|
+
"""
|
362
|
+
An action runtime skip.
|
351
363
|
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
364
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
365
|
+
"""
|
366
|
+
|
367
|
+
@property
|
368
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
369
|
+
return self._result | dict(
|
370
|
+
skipped=True,
|
371
|
+
msg=self.message,
|
372
|
+
)
|
373
|
+
|
374
|
+
@property
|
375
|
+
def omit_failed_key(self) -> bool:
|
376
|
+
return True
|
377
|
+
|
378
|
+
@property
|
379
|
+
def omit_exception_key(self) -> bool:
|
380
|
+
return True
|
356
381
|
|
357
382
|
|
358
383
|
class AnsibleActionFail(AnsibleAction):
|
359
|
-
"""
|
384
|
+
"""
|
385
|
+
An action runtime failure.
|
360
386
|
|
361
|
-
|
362
|
-
|
363
|
-
|
387
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
388
|
+
"""
|
389
|
+
|
390
|
+
@property
|
391
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
392
|
+
return self._result | dict(
|
393
|
+
failed=True,
|
394
|
+
msg=self.message,
|
395
|
+
)
|
364
396
|
|
365
|
-
result_overrides = {'failed': True, 'msg': message}
|
366
|
-
# deprecated: description='use sys.exception()' python_version='3.11'
|
367
|
-
if sys.exc_info()[1]: # DTFIX-RELEASE: remove this hack once TaskExecutor is no longer shucking AnsibleActionFail and returning its result
|
368
|
-
result_overrides['exception'] = traceback.format_exc()
|
369
397
|
|
370
|
-
|
398
|
+
class _ActionDone(AnsibleAction):
|
399
|
+
"""
|
400
|
+
Imports as `_AnsibleActionDone` are deprecated. An action runtime early exit.
|
401
|
+
|
402
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
403
|
+
"""
|
371
404
|
|
405
|
+
@property
|
406
|
+
def omit_failed_key(self) -> bool:
|
407
|
+
return not self._result.get('failed')
|
372
408
|
|
373
|
-
|
374
|
-
|
409
|
+
@property
|
410
|
+
def omit_exception_key(self) -> bool:
|
411
|
+
return not self._result.get('failed')
|
375
412
|
|
376
413
|
|
377
414
|
class AnsiblePluginError(AnsibleError):
|
@@ -422,13 +459,23 @@ def __getattr__(name: str) -> t.Any:
|
|
422
459
|
"""Inject import-time deprecation warnings."""
|
423
460
|
from ..utils.display import Display
|
424
461
|
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
462
|
+
match name:
|
463
|
+
case 'AnsibleFilterTypeError':
|
464
|
+
Display().deprecated(
|
465
|
+
msg=f"Importing {name!r} is deprecated.",
|
466
|
+
help_text=f"Import {AnsibleTypeError.__name__!r} instead.",
|
467
|
+
version="2.23",
|
468
|
+
)
|
469
|
+
|
470
|
+
return AnsibleTypeError
|
471
|
+
|
472
|
+
case '_AnsibleActionDone':
|
473
|
+
Display().deprecated(
|
474
|
+
msg=f"Importing {name!r} is deprecated.",
|
475
|
+
help_text="Return directly from action plugins instead.",
|
476
|
+
version="2.23",
|
477
|
+
)
|
431
478
|
|
432
|
-
|
479
|
+
return _ActionDone
|
433
480
|
|
434
481
|
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|