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.
Files changed (174) hide show
  1. ansible/_internal/__init__.py +2 -2
  2. ansible/_internal/_collection_proxy.py +1 -1
  3. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  4. ansible/_internal/_errors/_captured.py +25 -30
  5. ansible/_internal/_errors/_error_factory.py +89 -0
  6. ansible/_internal/_errors/_error_utils.py +240 -0
  7. ansible/_internal/_errors/_task_timeout.py +28 -0
  8. ansible/_internal/_event_formatting.py +127 -0
  9. ansible/_internal/_json/__init__.py +6 -6
  10. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  11. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  12. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  13. ansible/_internal/_ssh/__init__.py +0 -0
  14. ansible/_internal/_ssh/_agent_launch.py +91 -0
  15. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  16. ansible/_internal/_templating/__init__.py +5 -3
  17. ansible/_internal/_templating/_datatag.py +2 -1
  18. ansible/_internal/_templating/_engine.py +3 -4
  19. ansible/_internal/_templating/_jinja_bits.py +21 -16
  20. ansible/_internal/_templating/_jinja_common.py +18 -27
  21. ansible/_internal/_templating/_jinja_plugins.py +31 -3
  22. ansible/_internal/_templating/_lazy_containers.py +5 -5
  23. ansible/_internal/_templating/_transform.py +20 -19
  24. ansible/_internal/_templating/_utils.py +1 -1
  25. ansible/_internal/_testing.py +26 -0
  26. ansible/_internal/_yaml/_dumper.py +1 -1
  27. ansible/_internal/_yaml/_errors.py +7 -7
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  29. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  30. ansible/cli/__init__.py +5 -82
  31. ansible/cli/arguments/option_helpers.py +8 -5
  32. ansible/cli/doc.py +84 -28
  33. ansible/cli/inventory.py +1 -1
  34. ansible/compat/importlib_resources.py +9 -12
  35. ansible/config/base.yml +27 -23
  36. ansible/config/manager.py +142 -101
  37. ansible/constants.py +1 -1
  38. ansible/errors/__init__.py +96 -49
  39. ansible/executor/module_common.py +8 -10
  40. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  41. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  42. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  43. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  44. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  45. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  46. ansible/executor/powershell/module_manifest.py +52 -0
  47. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  48. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  49. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  50. ansible/executor/process/worker.py +38 -113
  51. ansible/executor/task_executor.py +26 -61
  52. ansible/executor/task_result.py +2 -4
  53. ansible/galaxy/collection/__init__.py +1 -4
  54. ansible/inventory/manager.py +1 -0
  55. ansible/module_utils/_internal/__init__.py +0 -3
  56. ansible/module_utils/_internal/_ambient_context.py +3 -3
  57. ansible/module_utils/_internal/_ansiballz.py +4 -2
  58. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  59. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  60. ansible/module_utils/_internal/_deprecator.py +66 -48
  61. ansible/module_utils/_internal/_errors.py +88 -17
  62. ansible/module_utils/_internal/_event_utils.py +61 -0
  63. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  64. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  65. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  66. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  67. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  68. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  69. ansible/module_utils/_internal/_plugin_info.py +1 -1
  70. ansible/module_utils/_internal/_stack.py +22 -0
  71. ansible/module_utils/_internal/_text_utils.py +6 -0
  72. ansible/module_utils/_internal/_traceback.py +11 -8
  73. ansible/module_utils/ansible_release.py +1 -1
  74. ansible/module_utils/basic.py +49 -15
  75. ansible/module_utils/common/arg_spec.py +2 -2
  76. ansible/module_utils/common/collections.py +6 -0
  77. ansible/module_utils/common/json.py +2 -2
  78. ansible/module_utils/common/text/converters.py +3 -3
  79. ansible/module_utils/common/validation.py +1 -1
  80. ansible/module_utils/common/warnings.py +80 -23
  81. ansible/module_utils/common/yaml.py +1 -1
  82. ansible/module_utils/datatag.py +5 -2
  83. ansible/module_utils/facts/system/distribution.py +16 -3
  84. ansible/module_utils/facts/virtual/linux.py +2 -2
  85. ansible/module_utils/parsing/convert_bool.py +6 -0
  86. ansible/module_utils/service.py +2 -9
  87. ansible/modules/apt_repository.py +7 -29
  88. ansible/modules/assemble.py +4 -4
  89. ansible/modules/async_status.py +13 -11
  90. ansible/modules/async_wrapper.py +5 -5
  91. ansible/modules/cron.py +3 -5
  92. ansible/modules/dnf5.py +15 -22
  93. ansible/modules/git.py +1 -6
  94. ansible/modules/hostname.py +0 -1
  95. ansible/modules/pip.py +2 -4
  96. ansible/modules/service.py +3 -9
  97. ansible/modules/sysvinit.py +3 -3
  98. ansible/parsing/ajson.py +3 -5
  99. ansible/parsing/dataloader.py +4 -4
  100. ansible/parsing/mod_args.py +1 -1
  101. ansible/parsing/plugin_docs.py +2 -2
  102. ansible/parsing/utils/yaml.py +3 -3
  103. ansible/parsing/vault/__init__.py +4 -4
  104. ansible/playbook/playbook_include.py +1 -1
  105. ansible/playbook/taggable.py +0 -3
  106. ansible/plugins/__init__.py +0 -25
  107. ansible/plugins/action/__init__.py +9 -32
  108. ansible/plugins/action/add_host.py +1 -1
  109. ansible/plugins/action/assemble.py +8 -16
  110. ansible/plugins/action/async_status.py +7 -2
  111. ansible/plugins/action/copy.py +8 -7
  112. ansible/plugins/action/gather_facts.py +8 -8
  113. ansible/plugins/action/package.py +5 -8
  114. ansible/plugins/action/script.py +8 -15
  115. ansible/plugins/action/service.py +3 -7
  116. ansible/plugins/action/template.py +6 -8
  117. ansible/plugins/action/unarchive.py +5 -15
  118. ansible/plugins/action/uri.py +9 -20
  119. ansible/plugins/callback/__init__.py +4 -6
  120. ansible/plugins/callback/junit.py +4 -2
  121. ansible/plugins/connection/local.py +2 -2
  122. ansible/plugins/connection/ssh.py +17 -9
  123. ansible/plugins/connection/winrm.py +5 -2
  124. ansible/plugins/doc_fragments/constructed.py +2 -2
  125. ansible/plugins/filter/core.py +13 -6
  126. ansible/plugins/filter/encryption.py +4 -4
  127. ansible/plugins/inventory/__init__.py +11 -10
  128. ansible/plugins/inventory/script.py +1 -1
  129. ansible/plugins/list.py +69 -16
  130. ansible/plugins/loader.py +10 -9
  131. ansible/plugins/lookup/csvfile.py +16 -71
  132. ansible/plugins/lookup/first_found.py +2 -1
  133. ansible/plugins/shell/__init__.py +56 -2
  134. ansible/plugins/shell/powershell.py +66 -9
  135. ansible/plugins/shell/sh.py +9 -5
  136. ansible/plugins/test/core.py +21 -15
  137. ansible/plugins/test/finished.yml +1 -1
  138. ansible/plugins/test/uri.py +2 -5
  139. ansible/release.py +1 -1
  140. ansible/template/__init__.py +30 -2
  141. ansible/utils/collection_loader/__init__.py +2 -0
  142. ansible/utils/display.py +107 -128
  143. ansible/utils/hashing.py +0 -1
  144. ansible/utils/listify.py +6 -4
  145. ansible/utils/plugin_docs.py +2 -1
  146. ansible/utils/unsafe_proxy.py +1 -1
  147. ansible/vars/hostvars.py +1 -1
  148. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
  149. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
  150. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  151. ansible_test/_data/completion/docker.txt +3 -3
  152. ansible_test/_data/completion/remote.txt +1 -0
  153. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  154. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  155. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  156. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  157. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  158. ansible_test/_internal/util.py +20 -0
  159. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  160. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  161. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  162. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  163. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  164. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
  165. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  166. ansible/_internal/_errors/_utils.py +0 -310
  167. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  168. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
  169. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
  170. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
  171. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
  172. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
  173. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
  174. {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
- # FIXME: see if we can unify in module_utils with similar function used by argspec
69
- def ensure_type(value, value_type, origin=None, origin_ftype=None):
70
- """ return a configuration variable with casting
71
- :arg value: The value to ensure correct typing of
72
- :kwarg value_type: The type of the value. This can be any of the following strings:
73
- :boolean: sets the value to a True or False value
74
- :bool: Same as 'boolean'
75
- :integer: Sets the value to an integer or raises a ValueType error
76
- :int: Same as 'integer'
77
- :float: Sets the value to a float or raises a ValueType error
78
- :list: Treats the value as a comma separated list. Split the value
79
- and return it as a python list.
80
- :none: Sets the value to None
81
- :path: Expands any environment variables and tilde's in the value.
82
- :tmppath: Create a unique temporary directory inside of the directory
83
- specified by value and return its path.
84
- :temppath: Same as 'tmppath'
85
- :tmp: Same as 'tmppath'
86
- :pathlist: Treat the value as a typical PATH string. (On POSIX, this
87
- means comma separated strings.) Split the value and then expand
88
- each part for environment variables and tildes.
89
- :pathspec: Treat the value as a PATH string. Expands any environment variables
90
- tildes's in the value.
91
- :str: Sets the value to string types.
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
- errmsg = ''
96
- basedir = None
97
- if origin and os.path.isabs(origin) and os.path.exists(to_bytes(origin)):
98
- basedir = origin
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
- if value is not None:
104
- if value_type in ('boolean', 'bool'):
105
- value = boolean(value, strict=False)
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
- elif value_type in ('integer', 'int'):
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
- value = int_part
112
- else:
113
- errmsg = 'int'
114
- except decimal.DecimalException:
115
- errmsg = 'int'
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
- elif value_type == 'float':
118
- if not isinstance(value, float):
119
- value = float(value)
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
- elif value_type == 'list':
122
- if isinstance(value, string_types):
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
- elif value_type == 'none':
164
+ if isinstance(value, Sequence) and not isinstance(value, bytes):
165
+ return list(value)
166
+
167
+ case 'none':
128
168
  if value == "None":
129
- value = None
169
+ return None
130
170
 
131
- if value is not None:
132
- errmsg = 'None'
171
+ case 'path':
172
+ if isinstance(value, str):
173
+ return resolve_path(value, basedir=basedir)
133
174
 
134
- elif value_type == 'path':
135
- if isinstance(value, string_types):
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
- elif value_type == 'pathspec':
152
- if isinstance(value, string_types):
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
- value = [resolve_path(x, basedir=basedir) for x in value]
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
- elif value_type == 'pathlist':
161
- if isinstance(value, string_types):
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
- value = [resolve_path(x, basedir=basedir) for x in value]
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
- elif value_type in ('dict', 'dictionary'):
170
- if not isinstance(value, Mapping):
171
- errmsg = 'dictionary'
202
+ case 'dictionary' | 'dict':
203
+ if isinstance(value, dict):
204
+ return value
172
205
 
173
- elif value_type in ('str', 'string'):
174
- if isinstance(value, (string_types, bool, int, float, complex)):
175
- value = to_text(value, errors='surrogate_or_strict')
176
- if origin_ftype and origin_ftype == 'ini':
177
- value = unquote(value)
178
- else:
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
- # defaults to string type
182
- elif isinstance(value, (string_types)):
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
- if errmsg:
188
- raise ValueError(f'Invalid type provided for {errmsg!r}: {value!r}')
216
+ if isinstance(value, _EncryptedStringProtocol):
217
+ return value._decrypt()
189
218
 
190
- return to_text(value, errors='surrogate_or_strict', nonstring='passthru')
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(object):
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, string_types) and (value.startswith('{{') and value.endswith('}}')) and variables is not None:
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
- t = NativeEnvironment().from_string(value)
378
- value = t.render(variables)
379
- except Exception:
380
- pass # not templatable
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'], string_types):
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 = ('.pyc', '.pyo', '.swp', '.bak', '~', '.rpm', '.md', '.txt', '.rst')
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
 
@@ -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 _utils
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 = _utils.concat_message(self._default_message, 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
- If `include_cause_message` is False, return the original message.
112
- Otherwise, return the original message with cause message(s) appended, stopping on (and including) the first non-AnsibleError.
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 _utils.get_chained_message(self)
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 _utils.RedactAnnotatedSourceContext.when(not self._show_content):
125
- if source_context := _utils.SourceContext.from_value(self.obj):
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
- """The transport / connection_plugin had a fatal error."""
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
- # These Exceptions are temporary, using them as flow control until we can get a better solution.
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(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content,
342
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
343
- if result is None:
344
- self.result = {}
345
- else:
346
- self.result = result
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
- """An action runtime skip."""
361
+ """
362
+ An action runtime skip.
351
363
 
352
- def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
353
- super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content,
354
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
355
- self.result.update({'skipped': True, 'msg': message})
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
- """An action runtime failure."""
384
+ """
385
+ An action runtime failure.
360
386
 
361
- def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
362
- super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content,
363
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
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
- self.result.update(result_overrides)
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
- class _AnsibleActionDone(AnsibleAction):
374
- """An action runtime early exit."""
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
- if name == 'AnsibleFilterTypeError':
426
- Display().deprecated(
427
- msg="Importing 'AnsibleFilterTypeError' is deprecated.",
428
- help_text=f"Import {AnsibleTypeError.__name__!r} instead.",
429
- version="2.23",
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
- return AnsibleTypeError
479
+ return _ActionDone
433
480
 
434
481
  raise AttributeError(f'module {__name__!r} has no attribute {name!r}')