ansible-core 2.19.0b6__py3-none-any.whl → 2.19.0rc1__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 (111) hide show
  1. ansible/_internal/_json/__init__.py +31 -20
  2. ansible/_internal/_json/_profiles/_legacy.py +1 -1
  3. ansible/_internal/_templating/_jinja_bits.py +46 -14
  4. ansible/_internal/_templating/_jinja_common.py +1 -1
  5. ansible/_internal/_templating/_jinja_plugins.py +5 -2
  6. ansible/_internal/_templating/_utils.py +2 -1
  7. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/dump_object.py +9 -0
  8. ansible/cli/__init__.py +2 -2
  9. ansible/cli/_ssh_askpass.py +37 -30
  10. ansible/cli/adhoc.py +6 -3
  11. ansible/cli/console.py +2 -2
  12. ansible/cli/doc.py +2 -2
  13. ansible/config/base.yml +9 -6
  14. ansible/executor/module_common.py +9 -6
  15. ansible/executor/powershell/psrp_put_file.ps1 +1 -1
  16. ansible/executor/task_executor.py +2 -2
  17. ansible/executor/task_queue_manager.py +34 -70
  18. ansible/executor/task_result.py +1 -1
  19. ansible/galaxy/api.py +2 -2
  20. ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
  21. ansible/galaxy/dependency_resolution/providers.py +3 -3
  22. ansible/inventory/group.py +6 -1
  23. ansible/inventory/host.py +6 -1
  24. ansible/module_utils/_internal/_datatag/__init__.py +6 -1
  25. ansible/module_utils/_internal/_deprecator.py +12 -1
  26. ansible/module_utils/ansible_release.py +1 -1
  27. ansible/module_utils/basic.py +14 -16
  28. ansible/module_utils/common/yaml.py +1 -1
  29. ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
  30. ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
  31. ansible/module_utils/facts/hardware/base.py +1 -1
  32. ansible/module_utils/facts/other/facter.py +1 -1
  33. ansible/module_utils/facts/system/distribution.py +2 -2
  34. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  35. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  36. ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
  37. ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
  38. ansible/module_utils/urls.py +1 -1
  39. ansible/modules/apt.py +9 -3
  40. ansible/modules/assemble.py +5 -3
  41. ansible/modules/expect.py +5 -5
  42. ansible/modules/hostname.py +2 -2
  43. ansible/modules/pip.py +9 -11
  44. ansible/modules/raw.py +2 -2
  45. ansible/modules/stat.py +1 -1
  46. ansible/modules/systemd.py +1 -1
  47. ansible/modules/systemd_service.py +1 -1
  48. ansible/modules/wait_for.py +10 -3
  49. ansible/parsing/mod_args.py +38 -20
  50. ansible/parsing/vault/__init__.py +3 -3
  51. ansible/playbook/base.py +0 -2
  52. ansible/playbook/helpers.py +1 -1
  53. ansible/playbook/playbook_include.py +23 -57
  54. ansible/playbook/role/__init__.py +40 -23
  55. ansible/plugins/action/__init__.py +2 -2
  56. ansible/plugins/action/assemble.py +2 -1
  57. ansible/plugins/action/assert.py +2 -2
  58. ansible/plugins/action/script.py +5 -4
  59. ansible/plugins/action/template.py +1 -1
  60. ansible/plugins/callback/__init__.py +77 -87
  61. ansible/plugins/callback/default.py +0 -3
  62. ansible/plugins/callback/junit.py +0 -6
  63. ansible/plugins/connection/ssh.py +13 -6
  64. ansible/plugins/filter/pow.yml +1 -1
  65. ansible/plugins/filter/root.yml +1 -1
  66. ansible/plugins/filter/strftime.yml +3 -3
  67. ansible/plugins/filter/to_uuid.yml +1 -1
  68. ansible/plugins/inventory/script.py +1 -1
  69. ansible/plugins/loader.py +5 -0
  70. ansible/plugins/lookup/password.py +4 -6
  71. ansible/release.py +1 -1
  72. ansible/utils/display.py +16 -26
  73. ansible/utils/path.py +1 -1
  74. ansible/utils/vars.py +6 -2
  75. ansible/vars/manager.py +6 -3
  76. ansible/vars/reserved.py +6 -4
  77. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/METADATA +1 -1
  78. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/RECORD +111 -109
  79. ansible_test/_internal/__init__.py +5 -0
  80. ansible_test/_internal/ansible_util.py +1 -1
  81. ansible_test/_internal/classification/python.py +6 -0
  82. ansible_test/_internal/cli/commands/__init__.py +0 -5
  83. ansible_test/_internal/cli/environments.py +51 -5
  84. ansible_test/_internal/commands/coverage/__init__.py +1 -1
  85. ansible_test/_internal/commands/integration/__init__.py +18 -5
  86. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  87. ansible_test/_internal/commands/sanity/__init__.py +3 -1
  88. ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
  89. ansible_test/_internal/commands/shell/__init__.py +43 -4
  90. ansible_test/_internal/commands/units/__init__.py +4 -1
  91. ansible_test/_internal/config.py +21 -13
  92. ansible_test/_internal/debugging.py +166 -0
  93. ansible_test/_internal/delegation.py +21 -13
  94. ansible_test/_internal/host_profiles.py +197 -6
  95. ansible_test/_internal/inventory.py +4 -0
  96. ansible_test/_internal/metadata.py +94 -4
  97. ansible_test/_internal/processes.py +80 -0
  98. ansible_test/_internal/python_requirements.py +27 -0
  99. ansible_test/_internal/target.py +8 -0
  100. ansible_test/_internal/util_common.py +13 -3
  101. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +2 -1
  102. ansible_test/_util/target/injector/python.py +8 -0
  103. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/WHEEL +0 -0
  104. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/entry_points.txt +0 -0
  105. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/COPYING +0 -0
  106. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  107. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  108. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  109. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  110. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  111. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/top_level.txt +0 -0
@@ -81,6 +81,7 @@ class AnsibleVariableVisitor:
81
81
  convert_custom_scalars: bool = False,
82
82
  convert_to_native_values: bool = False,
83
83
  apply_transforms: bool = False,
84
+ visit_keys: bool = False,
84
85
  encrypted_string_behavior: EncryptedStringBehavior = EncryptedStringBehavior.DECRYPT,
85
86
  ):
86
87
  super().__init__() # supports StateTrackingMixIn
@@ -92,6 +93,7 @@ class AnsibleVariableVisitor:
92
93
  self.convert_custom_scalars = convert_custom_scalars
93
94
  self.convert_to_native_values = convert_to_native_values
94
95
  self.apply_transforms = apply_transforms
96
+ self.visit_keys = visit_keys
95
97
  self.encrypted_string_behavior = encrypted_string_behavior
96
98
 
97
99
  if apply_transforms:
@@ -134,47 +136,55 @@ class AnsibleVariableVisitor:
134
136
 
135
137
  return result
136
138
 
139
+ def _visit_key(self, key: t.Any) -> t.Any:
140
+ """Internal implementation to recursively visit a key if visit_keys is enabled."""
141
+ if not self.visit_keys:
142
+ return key
143
+
144
+ return self._visit(None, key) # key=None prevents state tracking from seeing the key as value
145
+
137
146
  def _visit(self, key: t.Any, value: _T) -> _T:
138
147
  """Internal implementation to recursively visit a data structure's contents."""
139
148
  self._current = key # supports StateTrackingMixIn
140
149
 
141
- value_type = type(value)
150
+ value_type: type = type(value)
142
151
 
143
- if self.apply_transforms and value_type in _transform._type_transform_mapping:
152
+ # handle EncryptedString conversion before more generic transformation and native conversions
153
+ if value_type is EncryptedString: # pylint: disable=unidiomatic-typecheck
154
+ match self.encrypted_string_behavior:
155
+ case EncryptedStringBehavior.DECRYPT:
156
+ value = str(value) # type: ignore[assignment]
157
+ value_type = str
158
+ case EncryptedStringBehavior.REDACT:
159
+ value = "<redacted>" # type: ignore[assignment]
160
+ value_type = str
161
+ case EncryptedStringBehavior.FAIL:
162
+ raise AnsibleVariableTypeError.from_value(obj=value)
163
+ elif self.apply_transforms and value_type in _transform._type_transform_mapping:
144
164
  value = self._template_engine.transform(value)
145
165
  value_type = type(value)
146
166
 
147
- # DTFIX3: need to handle native copy for keys too
148
167
  if self.convert_to_native_values and isinstance(value, _datatag.AnsibleTaggedObject):
149
168
  value = value._native_copy()
150
169
  value_type = type(value)
151
170
 
152
171
  result: _T
153
172
 
154
- # DTFIX3: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
155
- # keep in mind the allowed types for keys is a more restrictive set than for values (str and tagged str only, not EncryptedString)
156
- # DTFIX5: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
173
+ # DTFIX-FUTURE: Visitor generally ignores dict/mapping keys by default except for debugging and schema-aware checking.
174
+ # It could be checking keys destined for variable storage to apply more strict rules about key shape and type.
157
175
 
158
176
  if (result := self._early_visit(value, value_type)) is not _sentinel:
159
177
  pass
160
178
  # DTFIX7: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
161
179
  elif value_type in _ANSIBLE_ALLOWED_MAPPING_VAR_TYPES: # check mappings first, because they're also collections
162
180
  with self: # supports StateTrackingMixIn
163
- result = AnsibleTagHelper.tag_copy(value, ((k, self._visit(k, v)) for k, v in value.items()), value_type=value_type)
181
+ result = AnsibleTagHelper.tag_copy(value, ((self._visit_key(k), self._visit(k, v)) for k, v in value.items()), value_type=value_type)
164
182
  elif value_type in _ANSIBLE_ALLOWED_NON_SCALAR_COLLECTION_VAR_TYPES:
165
183
  with self: # supports StateTrackingMixIn
166
184
  result = AnsibleTagHelper.tag_copy(value, (self._visit(k, v) for k, v in enumerate(t.cast(t.Iterable, value))), value_type=value_type)
167
- elif self.encrypted_string_behavior != EncryptedStringBehavior.FAIL and isinstance(value, EncryptedString):
168
- match self.encrypted_string_behavior:
169
- case EncryptedStringBehavior.REDACT:
170
- result = "<redacted>" # type: ignore[assignment]
171
- case EncryptedStringBehavior.PRESERVE:
172
- result = value # type: ignore[assignment]
173
- case EncryptedStringBehavior.DECRYPT:
174
- result = str(value) # type: ignore[assignment]
175
185
  elif self.convert_mapping_to_dict and _internal.is_intermediate_mapping(value):
176
186
  with self: # supports StateTrackingMixIn
177
- result = {k: self._visit(k, v) for k, v in value.items()} # type: ignore[assignment]
187
+ result = {self._visit_key(k): self._visit(k, v) for k, v in value.items()} # type: ignore[assignment]
178
188
  elif self.convert_sequence_to_list and _internal.is_intermediate_iterable(value):
179
189
  with self: # supports StateTrackingMixIn
180
190
  result = [self._visit(k, v) for k, v in enumerate(t.cast(t.Iterable, value))] # type: ignore[assignment]
@@ -184,12 +194,13 @@ class AnsibleVariableVisitor:
184
194
  result = float(value) # type: ignore[assignment]
185
195
  elif self.convert_custom_scalars and isinstance(value, int) and not isinstance(value, bool):
186
196
  result = int(value) # type: ignore[assignment]
187
- else:
188
- if value_type not in _ANSIBLE_ALLOWED_VAR_TYPES:
189
- raise AnsibleVariableTypeError.from_value(obj=value)
190
-
197
+ elif value_type in _ANSIBLE_ALLOWED_VAR_TYPES:
191
198
  # supported scalar type that requires no special handling, just return as-is
192
199
  result = value
200
+ elif self.encrypted_string_behavior is EncryptedStringBehavior.PRESERVE and isinstance(value, EncryptedString):
201
+ result = value # type: ignore[assignment]
202
+ else:
203
+ raise AnsibleVariableTypeError.from_value(obj=value)
193
204
 
194
205
  if self.origin and not Origin.is_tagged_on(result):
195
206
  # apply shared instance default origin tag
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Backwards compatibility profile for serialization other than inventory (which should use inventory_legacy for backward-compatible trust behavior).
3
- Behavior is equivalent to pre 2.18 `AnsibleJSONEncoder` with vault_to_text=True.
3
+ Behavior is equivalent to pre 2.19 `AnsibleJSONEncoder` with vault_to_text=True.
4
4
  """
5
5
 
6
6
  from __future__ import annotations as _annotations
@@ -20,7 +20,7 @@ from jinja2.lexer import TOKEN_VARIABLE_BEGIN, TOKEN_VARIABLE_END, TOKEN_STRING,
20
20
  from jinja2.nativetypes import NativeCodeGenerator
21
21
  from jinja2.nodes import Const, EvalContext
22
22
  from jinja2.runtime import Context, Macro
23
- from jinja2.sandbox import ImmutableSandboxedEnvironment
23
+ from jinja2.sandbox import SandboxedEnvironment
24
24
  from jinja2.utils import missing, LRUCache
25
25
 
26
26
  from ansible.utils.display import Display
@@ -78,6 +78,21 @@ The values following this prefix up to the first newline are parsed as Jinja2 te
78
78
  To include this literal value at the start of a string, a space or other character must precede it.
79
79
  """
80
80
 
81
+ JINJA_KEYWORDS = frozenset(
82
+ {
83
+ # scalar singletons (see jinja2.nodes.Name.can_assign)
84
+ 'true',
85
+ 'false',
86
+ 'none',
87
+ 'True',
88
+ 'False',
89
+ 'None',
90
+ # other
91
+ 'not', # unary operator always applicable to names
92
+ }
93
+ )
94
+ """Names which have special meaning to Jinja and cannot be resolved as variable names."""
95
+
81
96
  display = Display()
82
97
 
83
98
 
@@ -503,9 +518,6 @@ def create_template_error(ex: Exception, variable: t.Any, is_expression: bool) -
503
518
  return exception_to_raise
504
519
 
505
520
 
506
- # DTFIX3: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
507
- # also update the protomatter integration test once this is done (the test was written differently since this wasn't done yet)
508
-
509
521
  _BUILTIN_FILTER_ALIASES: dict[str, str] = {}
510
522
  _BUILTIN_TEST_ALIASES: dict[str, str] = {
511
523
  '!=': 'ne',
@@ -520,7 +532,7 @@ _BUILTIN_FILTERS = filter_loader._wrap_funcs(defaults.DEFAULT_FILTERS, _BUILTIN_
520
532
  _BUILTIN_TESTS = test_loader._wrap_funcs(t.cast(dict[str, t.Callable], defaults.DEFAULT_TESTS), _BUILTIN_TEST_ALIASES)
521
533
 
522
534
 
523
- class AnsibleEnvironment(ImmutableSandboxedEnvironment):
535
+ class AnsibleEnvironment(SandboxedEnvironment):
524
536
  """
525
537
  Our custom environment, which simply allows us to override the class-level
526
538
  values for the Template and Context classes used by jinja2 internally.
@@ -531,6 +543,21 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
531
543
  code_generator_class = AnsibleCodeGenerator
532
544
  intercepted_binops = frozenset(('eq',))
533
545
 
546
+ _allowed_unsafe_attributes: dict[str, type | tuple[type, ...]] = dict(
547
+ # Allow bitwise operations on int until bitwise filters are available.
548
+ # see: https://github.com/ansible/ansible/issues/85204
549
+ __and__=int,
550
+ __lshift__=int,
551
+ __or__=int,
552
+ __rshift__=int,
553
+ __xor__=int,
554
+ )
555
+ """
556
+ Attributes which are considered unsafe by `is_safe_attribute`, which should be allowed when used on specific types.
557
+ The attributes allowed here are intended only for backward compatibility with existing use cases.
558
+ They should be exposed as filters in a future release and eventually deprecated.
559
+ """
560
+
534
561
  _lexer_cache = LRUCache(50)
535
562
 
536
563
  # DTFIX-FUTURE: bikeshed a name/mechanism to control template debugging
@@ -594,6 +621,9 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
594
621
  if _TemplateConfig.sandbox_mode == _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES:
595
622
  return True
596
623
 
624
+ if (type_or_tuple := self._allowed_unsafe_attributes.get(attr)) and isinstance(obj, type_or_tuple):
625
+ return True
626
+
597
627
  return super().is_safe_attribute(obj, attr, value)
598
628
 
599
629
  @property
@@ -794,18 +824,18 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
794
824
  *args: t.Any,
795
825
  **kwargs: t.Any,
796
826
  ) -> t.Any:
797
- if _DirectCall.is_marked(__obj):
798
- # Both `_lookup` and `_query` handle arg proxying and `Marker` args internally.
799
- # Performing either before calling them will interfere with that processing.
800
- return super().call(__context, __obj, *args, **kwargs)
827
+ try:
828
+ if _DirectCall.is_marked(__obj):
829
+ # Both `_lookup` and `_query` handle arg proxying and `Marker` args internally.
830
+ # Performing either before calling them will interfere with that processing.
831
+ return super().call(__context, __obj, *args, **kwargs)
801
832
 
802
- # Jinja's generated macro code handles Markers, so pre-emptive raise on Marker args and lazy retrieval should be disabled for the macro invocation.
803
- is_macro = isinstance(__obj, Macro)
833
+ # Jinja's generated macro code handles Markers, so preemptive raise on Marker args and lazy retrieval should be disabled for the macro invocation.
834
+ is_macro = isinstance(__obj, Macro)
804
835
 
805
- if not is_macro and (first_marker := get_first_marker_arg(args, kwargs)) is not None:
806
- return first_marker
836
+ if not is_macro and (first_marker := get_first_marker_arg(args, kwargs)) is not None:
837
+ return first_marker
807
838
 
808
- try:
809
839
  with JinjaCallContext(accept_lazy_markers=is_macro):
810
840
  call_res = super().call(__context, __obj, *lazify_container_args(args), **lazify_container_kwargs(kwargs))
811
841
 
@@ -819,6 +849,8 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
819
849
 
820
850
  except MarkerError as ex:
821
851
  return ex.source
852
+ except Exception as ex:
853
+ return CapturedExceptionMarker(ex)
822
854
 
823
855
 
824
856
  AnsibleTemplate.environment_class = AnsibleEnvironment
@@ -96,7 +96,7 @@ class Marker(StrictUndefined, Tripwire):
96
96
  return AnsibleUndefinedVariable(self._undefined_message, obj=self._marker_template_source)
97
97
 
98
98
  def _as_message(self) -> str:
99
- """Return the error message to show when this marker must be represented as a string, such as for subsitutions or warnings."""
99
+ """Return the error message to show when this marker must be represented as a string, such as for substitutions or warnings."""
100
100
  return self._undefined_message
101
101
 
102
102
  def _fail_with_undefined_error(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
@@ -23,7 +23,7 @@ from ansible.utils.display import Display
23
23
 
24
24
  from ._datatag import _JinjaConstTemplate
25
25
  from ._errors import AnsibleTemplatePluginRuntimeError, AnsibleTemplatePluginLoadError, AnsibleTemplatePluginNotFoundError
26
- from ._jinja_common import MarkerError, _TemplateConfig, get_first_marker_arg, Marker, JinjaCallContext
26
+ from ._jinja_common import MarkerError, _TemplateConfig, get_first_marker_arg, Marker, JinjaCallContext, CapturedExceptionMarker
27
27
  from ._lazy_containers import lazify_container_kwargs, lazify_container_args, lazify_container, _AnsibleLazyTemplateMixin
28
28
  from ._utils import LazyOptions, TemplateContext
29
29
 
@@ -119,7 +119,10 @@ class JinjaPluginIntercept(c.MutableMapping):
119
119
  except MarkerError as ex:
120
120
  return ex.source
121
121
  except Exception as ex:
122
- raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-FUTURE: which name to use? use plugin info?
122
+ try:
123
+ raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-FUTURE: which name to use? PluginInfo?
124
+ except AnsibleTemplatePluginRuntimeError as captured:
125
+ return CapturedExceptionMarker(captured)
123
126
 
124
127
  def _wrap_test(self, instance: AnsibleJinja2Plugin) -> t.Callable:
125
128
  """Intercept point for all test plugins to ensure that args are properly templated/lazified."""
@@ -99,9 +99,10 @@ Omit = object.__new__(_OmitType)
99
99
  _datatag._untaggable_types.add(_OmitType)
100
100
 
101
101
 
102
- # DTFIX5: review these type sets to ensure they're not overly permissive/dynamic
103
102
  IGNORE_SCALAR_VAR_TYPES = {value for value in _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES if not issubclass(value, str)}
103
+ """Scalar variable types that short-circuit bypass templating."""
104
104
 
105
105
  PASS_THROUGH_SCALAR_VAR_TYPES = _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES | {
106
106
  _OmitType, # allow pass through of omit for later handling after top-level finalize completes
107
107
  }
108
+ """Scalar variable types which are allowed to appear in finalized template results."""
@@ -3,12 +3,21 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  import typing as t
5
5
 
6
+ from ansible.template import accept_args_markers
7
+ from ansible._internal._templating._jinja_common import ExceptionMarker
6
8
 
9
+
10
+ @accept_args_markers
7
11
  def dump_object(value: t.Any) -> object:
8
12
  """Internal filter to convert objects not supported by JSON to types which are."""
9
13
  if dataclasses.is_dataclass(value):
10
14
  return dataclasses.asdict(value) # type: ignore[arg-type]
11
15
 
16
+ if isinstance(value, ExceptionMarker):
17
+ return dict(
18
+ exception=value._as_exception(),
19
+ )
20
+
12
21
  return value
13
22
 
14
23
 
ansible/cli/__init__.py CHANGED
@@ -212,9 +212,9 @@ class CLI(ABC):
212
212
  # used by --vault-id and --vault-password-file
213
213
  vault_ids.append(id_slug)
214
214
 
215
- # if an action needs an encrypt password (create_new_password=True) and we dont
215
+ # if an action needs an encrypt password (create_new_password=True) and we don't
216
216
  # have other secrets setup, then automatically add a password prompt as well.
217
- # prompts cant/shouldnt work without a tty, so dont add prompt secrets
217
+ # prompts can't/shouldn't work without a tty, so don't add prompt secrets
218
218
  if ask_vault_pass or (not vault_ids and auto_prompt):
219
219
 
220
220
  id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, u'prompt_ask_vault_pass')
@@ -3,45 +3,52 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import multiprocessing.resource_tracker
6
7
  import os
7
8
  import re
8
9
  import sys
9
10
  import typing as t
10
- from multiprocessing.shared_memory import SharedMemory
11
11
 
12
- HOST_KEY_RE = re.compile(
13
- r'(The authenticity of host |differs from the key for the IP address)',
14
- )
12
+ from multiprocessing.shared_memory import SharedMemory
15
13
 
16
14
 
17
15
  def main() -> t.Never:
18
- try:
19
- if HOST_KEY_RE.search(sys.argv[1]):
20
- sys.stdout.buffer.write(b'no')
21
- sys.stdout.flush()
22
- sys.exit(0)
23
- except IndexError:
24
- pass
25
-
26
- kwargs: dict[str, bool] = {}
27
- if sys.version_info[:2] >= (3, 13):
28
- # deprecated: description='unneeded due to track argument for SharedMemory' python_version='3.12'
29
- kwargs['track'] = False
30
- try:
31
- shm = SharedMemory(name=os.environ['_ANSIBLE_SSH_ASKPASS_SHM'], **kwargs)
32
- except FileNotFoundError:
33
- # We must be running after the ansible fork is shutting down
34
- sys.exit(1)
16
+ if len(sys.argv) > 1:
17
+ exit_code = 0 if handle_prompt(sys.argv[1]) else 1
18
+ else:
19
+ exit_code = 1
20
+
21
+ sys.exit(exit_code)
22
+
23
+
24
+ def handle_prompt(prompt: str) -> bool:
25
+ if re.search(r'(The authenticity of host |differs from the key for the IP address)', prompt):
26
+ sys.stdout.write('no')
27
+ sys.stdout.flush()
28
+ return True
29
+
30
+ # deprecated: description='Python 3.13 and later support track' python_version='3.12'
31
+ can_track = sys.version_info[:2] >= (3, 13)
32
+ kwargs = dict(track=False) if can_track else {}
33
+
34
+ # This SharedMemory instance is intentionally not closed or unlinked.
35
+ # Closing will occur naturally in the SharedMemory finalizer.
36
+ # Unlinking is the responsibility of the process which created it.
37
+ shm = SharedMemory(name=os.environ['_ANSIBLE_SSH_ASKPASS_SHM'], **kwargs)
38
+
39
+ if not can_track:
40
+ # When track=False is not available, we must unregister explicitly, since it otherwise only occurs during unlink.
41
+ # This avoids resource tracker noise on stderr during process exit.
42
+ multiprocessing.resource_tracker.unregister(shm._name, 'shared_memory')
43
+
35
44
  cfg = json.loads(shm.buf.tobytes().rstrip(b'\x00'))
36
45
 
37
- try:
38
- if cfg['prompt'] not in sys.argv[1]:
39
- sys.exit(1)
40
- except IndexError:
41
- sys.exit(1)
46
+ if cfg['prompt'] not in prompt:
47
+ return False
42
48
 
43
- sys.stdout.buffer.write(cfg['password'].encode('utf-8'))
49
+ # Report the password provided by the SharedMemory instance.
50
+ # The contents are left untouched after consumption to allow subsequent attempts to succeed.
51
+ # This can occur when multiple password prompting methods are enabled, such as password and keyboard-interactive, which is the default on macOS.
52
+ sys.stdout.write(cfg['password'])
44
53
  sys.stdout.flush()
45
- shm.buf[:] = b'\x00' * shm.size
46
- shm.close()
47
- sys.exit(0)
54
+ return True
ansible/cli/adhoc.py CHANGED
@@ -88,8 +88,11 @@ class AdHocCLI(CLI):
88
88
  if not module_args:
89
89
  module_args = parse_kv(module_args_raw, check_raw=check_raw)
90
90
 
91
- mytask = {'action': {'module': context.CLIARGS['module_name'], 'args': module_args},
92
- 'timeout': context.CLIARGS['task_timeout']}
91
+ mytask = dict(
92
+ action=context.CLIARGS['module_name'],
93
+ args=module_args,
94
+ timeout=context.CLIARGS['task_timeout'],
95
+ )
93
96
 
94
97
  mytask = Origin(description=f'<adhoc {context.CLIARGS["module_name"]!r} task>').tag(mytask)
95
98
 
@@ -184,7 +187,7 @@ class AdHocCLI(CLI):
184
187
  variable_manager=variable_manager,
185
188
  loader=loader,
186
189
  passwords=passwords,
187
- stdout_callback=cb,
190
+ stdout_callback_name=cb,
188
191
  run_additional_callbacks=C.DEFAULT_LOAD_CALLBACK_PLUGINS,
189
192
  run_tree=run_tree,
190
193
  forks=context.CLIARGS['forks'],
ansible/cli/console.py CHANGED
@@ -194,7 +194,7 @@ class ConsoleCLI(CLI, cmd.Cmd):
194
194
  result = None
195
195
  try:
196
196
  check_raw = module in C._ACTION_ALLOWS_RAW_ARGS
197
- task = dict(action=dict(module=module, args=parse_kv(module_args, check_raw=check_raw)), timeout=self.task_timeout)
197
+ task = dict(action=module, args=parse_kv(module_args, check_raw=check_raw), timeout=self.task_timeout)
198
198
  play_ds = dict(
199
199
  name="Ansible Shell",
200
200
  hosts=self.cwd,
@@ -222,7 +222,7 @@ class ConsoleCLI(CLI, cmd.Cmd):
222
222
  variable_manager=self.variable_manager,
223
223
  loader=self.loader,
224
224
  passwords=self.passwords,
225
- stdout_callback=cb,
225
+ stdout_callback_name=cb,
226
226
  run_additional_callbacks=C.DEFAULT_LOAD_CALLBACK_PLUGINS,
227
227
  run_tree=False,
228
228
  forks=self.forks,
ansible/cli/doc.py CHANGED
@@ -1309,7 +1309,7 @@ class DocCLI(CLI, RoleMixin):
1309
1309
  if ignore in item:
1310
1310
  del item[ignore]
1311
1311
 
1312
- # reformat cli optoins
1312
+ # reformat cli options
1313
1313
  if 'cli' in opt and opt['cli']:
1314
1314
  conf['cli'] = []
1315
1315
  for cli in opt['cli']:
@@ -1440,7 +1440,7 @@ class DocCLI(CLI, RoleMixin):
1440
1440
  pad = display.columns * 0.20
1441
1441
  limit = max(display.columns - int(pad), 70)
1442
1442
 
1443
- text.append("> %s %s (%s)" % (plugin_type.upper(), _format(doc.pop('plugin_name'), 'bold'), doc.pop('filename')))
1443
+ text.append("> %s %s (%s)" % (plugin_type.upper(), _format(doc.pop('plugin_name'), 'bold'), doc.pop('filename') or 'Jinja2'))
1444
1444
 
1445
1445
  if isinstance(doc['description'], list):
1446
1446
  descs = doc.pop('description')
ansible/config/base.yml CHANGED
@@ -41,6 +41,15 @@ _CALLBACK_DISPATCH_ERROR_BEHAVIOR:
41
41
  ignore: just continue silently
42
42
  env: [ { name: _ANSIBLE_CALLBACK_DISPATCH_ERROR_BEHAVIOR } ]
43
43
  version_added: '2.19'
44
+ _MODULE_METADATA:
45
+ name: Enable experimental module metadata
46
+ description:
47
+ - Enables experimental module-level metadata controls for serialization profile selection.
48
+ - This is for internal use only.
49
+ type: boolean
50
+ default: false
51
+ env: [ { name: _ANSIBLE_MODULE_METADATA } ]
52
+ version_added: '2.19'
44
53
  ALLOW_BROKEN_CONDITIONALS:
45
54
  # This config option will be deprecated once it no longer has any effect (2.23).
46
55
  name: Allow broken conditionals
@@ -2176,12 +2185,6 @@ WIN_ASYNC_STARTUP_TIMEOUT:
2176
2185
  vars:
2177
2186
  - {name: ansible_win_async_startup_timeout}
2178
2187
  version_added: '2.10'
2179
- WRAP_STDERR:
2180
- description: Control line-wrapping behavior on console warnings and errors from default output callbacks (eases pattern-based output testing)
2181
- env: [{name: ANSIBLE_WRAP_STDERR}]
2182
- default: false
2183
- type: bool
2184
- version_added: "2.19"
2185
2188
  YAML_FILENAME_EXTENSIONS:
2186
2189
  name: Valid YAML extensions
2187
2190
  default: [".yml", ".yaml", ".json"]
@@ -364,7 +364,7 @@ def _get_shebang(interpreter, task_vars, templar: _template.Templar, args=tuple(
364
364
  options=TemplateOptions(value_for_omit=None))
365
365
 
366
366
  if not interpreter_out:
367
- # nothing matched(None) or in case someone configures empty string or empty intepreter
367
+ # nothing matched(None) or in case someone configures empty string or empty interpreter
368
368
  interpreter_out = interpreter
369
369
 
370
370
  # set shebang
@@ -659,9 +659,14 @@ metadata_versions: dict[t.Any, type[ModuleMetadata]] = {
659
659
  1: ModuleMetadataV1,
660
660
  }
661
661
 
662
+ _DEFAULT_LEGACY_METADATA = ModuleMetadataV1(serialization_profile='legacy')
663
+
662
664
 
663
665
  def _get_module_metadata(module: ast.Module) -> ModuleMetadata:
664
- # DTFIX2: while module metadata works, this feature isn't fully baked and should be turned off before release
666
+ # experimental module metadata; off by default
667
+ if not C.config.get_config_value('_MODULE_METADATA'):
668
+ return _DEFAULT_LEGACY_METADATA
669
+
665
670
  metadata_nodes: list[ast.Assign] = []
666
671
 
667
672
  for node in module.body:
@@ -674,9 +679,7 @@ def _get_module_metadata(module: ast.Module) -> ModuleMetadata:
674
679
  metadata_nodes.append(node)
675
680
 
676
681
  if not metadata_nodes:
677
- return ModuleMetadataV1(
678
- serialization_profile='legacy',
679
- )
682
+ return _DEFAULT_LEGACY_METADATA
680
683
 
681
684
  if len(metadata_nodes) > 1:
682
685
  raise ValueError('Module METADATA must defined only once.')
@@ -951,7 +954,7 @@ class _BuiltModule:
951
954
  class _CachedModule:
952
955
  """Cached Python module created by AnsiballZ."""
953
956
 
954
- # DTFIX5: secure this (locked down pickle, don't use pickle, etc.)
957
+ # FIXME: switch this to use a locked down pickle config or don't use pickle- easy to mess up and reach objects that shouldn't be pickled
955
958
 
956
959
  zip_data: bytes
957
960
  metadata: ModuleMetadata
@@ -102,7 +102,7 @@ begin {
102
102
  Set-Property 'MaximumAllowedMemory' $null
103
103
  }
104
104
  catch {
105
- # Satify pslint, we purposefully ignore this error as it is not critical it works.
105
+ # Satisfy pslint, we purposefully ignore this error as it is not critical it works.
106
106
  $null = $null
107
107
  }
108
108
  }
@@ -872,7 +872,7 @@ class TaskExecutor:
872
872
  async_result = async_handler.run(task_vars=task_vars)
873
873
  # We do not bail out of the loop in cases where the failure
874
874
  # is associated with a parsing error. The async_runner can
875
- # have issues which result in a half-written/unparseable result
875
+ # have issues which result in a half-written/unparsable result
876
876
  # file on disk, which manifests to the user as a timeout happening
877
877
  # before it's time to timeout.
878
878
  if (async_result.get('finished', False) or
@@ -910,7 +910,7 @@ class TaskExecutor:
910
910
  if async_result.get('_ansible_parsed'):
911
911
  return dict(failed=True, msg="async task did not complete within the requested time - %ss" % self._task.async_val, async_result=async_result)
912
912
  else:
913
- return dict(failed=True, msg="async task produced unparseable results", async_result=async_result)
913
+ return dict(failed=True, msg="async task produced unparsable results", async_result=async_result)
914
914
  else:
915
915
  # If the async task finished, automatically cleanup the temporary
916
916
  # status file left behind.