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.
- ansible/_internal/_json/__init__.py +31 -20
- ansible/_internal/_json/_profiles/_legacy.py +1 -1
- ansible/_internal/_templating/_jinja_bits.py +46 -14
- ansible/_internal/_templating/_jinja_common.py +1 -1
- ansible/_internal/_templating/_jinja_plugins.py +5 -2
- ansible/_internal/_templating/_utils.py +2 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/dump_object.py +9 -0
- ansible/cli/__init__.py +2 -2
- ansible/cli/_ssh_askpass.py +37 -30
- ansible/cli/adhoc.py +6 -3
- ansible/cli/console.py +2 -2
- ansible/cli/doc.py +2 -2
- ansible/config/base.yml +9 -6
- ansible/executor/module_common.py +9 -6
- ansible/executor/powershell/psrp_put_file.ps1 +1 -1
- ansible/executor/task_executor.py +2 -2
- ansible/executor/task_queue_manager.py +34 -70
- ansible/executor/task_result.py +1 -1
- ansible/galaxy/api.py +2 -2
- ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
- ansible/galaxy/dependency_resolution/providers.py +3 -3
- ansible/inventory/group.py +6 -1
- ansible/inventory/host.py +6 -1
- ansible/module_utils/_internal/_datatag/__init__.py +6 -1
- ansible/module_utils/_internal/_deprecator.py +12 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +14 -16
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
- ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
- ansible/module_utils/facts/hardware/base.py +1 -1
- ansible/module_utils/facts/other/facter.py +1 -1
- ansible/module_utils/facts/system/distribution.py +2 -2
- ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
- ansible/module_utils/urls.py +1 -1
- ansible/modules/apt.py +9 -3
- ansible/modules/assemble.py +5 -3
- ansible/modules/expect.py +5 -5
- ansible/modules/hostname.py +2 -2
- ansible/modules/pip.py +9 -11
- ansible/modules/raw.py +2 -2
- ansible/modules/stat.py +1 -1
- ansible/modules/systemd.py +1 -1
- ansible/modules/systemd_service.py +1 -1
- ansible/modules/wait_for.py +10 -3
- ansible/parsing/mod_args.py +38 -20
- ansible/parsing/vault/__init__.py +3 -3
- ansible/playbook/base.py +0 -2
- ansible/playbook/helpers.py +1 -1
- ansible/playbook/playbook_include.py +23 -57
- ansible/playbook/role/__init__.py +40 -23
- ansible/plugins/action/__init__.py +2 -2
- ansible/plugins/action/assemble.py +2 -1
- ansible/plugins/action/assert.py +2 -2
- ansible/plugins/action/script.py +5 -4
- ansible/plugins/action/template.py +1 -1
- ansible/plugins/callback/__init__.py +77 -87
- ansible/plugins/callback/default.py +0 -3
- ansible/plugins/callback/junit.py +0 -6
- ansible/plugins/connection/ssh.py +13 -6
- ansible/plugins/filter/pow.yml +1 -1
- ansible/plugins/filter/root.yml +1 -1
- ansible/plugins/filter/strftime.yml +3 -3
- ansible/plugins/filter/to_uuid.yml +1 -1
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/loader.py +5 -0
- ansible/plugins/lookup/password.py +4 -6
- ansible/release.py +1 -1
- ansible/utils/display.py +16 -26
- ansible/utils/path.py +1 -1
- ansible/utils/vars.py +6 -2
- ansible/vars/manager.py +6 -3
- ansible/vars/reserved.py +6 -4
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/RECORD +111 -109
- ansible_test/_internal/__init__.py +5 -0
- ansible_test/_internal/ansible_util.py +1 -1
- ansible_test/_internal/classification/python.py +6 -0
- ansible_test/_internal/cli/commands/__init__.py +0 -5
- ansible_test/_internal/cli/environments.py +51 -5
- ansible_test/_internal/commands/coverage/__init__.py +1 -1
- ansible_test/_internal/commands/integration/__init__.py +18 -5
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/sanity/__init__.py +3 -1
- ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
- ansible_test/_internal/commands/shell/__init__.py +43 -4
- ansible_test/_internal/commands/units/__init__.py +4 -1
- ansible_test/_internal/config.py +21 -13
- ansible_test/_internal/debugging.py +166 -0
- ansible_test/_internal/delegation.py +21 -13
- ansible_test/_internal/host_profiles.py +197 -6
- ansible_test/_internal/inventory.py +4 -0
- ansible_test/_internal/metadata.py +94 -4
- ansible_test/_internal/processes.py +80 -0
- ansible_test/_internal/python_requirements.py +27 -0
- ansible_test/_internal/target.py +8 -0
- ansible_test/_internal/util_common.py +13 -3
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +2 -1
- ansible_test/_util/target/injector/python.py +8 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {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
|
-
|
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
|
-
#
|
155
|
-
#
|
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
|
-
|
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.
|
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
|
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(
|
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
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
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
|
-
|
803
|
-
|
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
|
-
|
806
|
-
|
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
|
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
|
-
|
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
|
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
|
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')
|
ansible/cli/_ssh_askpass.py
CHANGED
@@ -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
|
-
|
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
|
-
|
19
|
-
if
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
39
|
-
sys.exit(1)
|
40
|
-
except IndexError:
|
41
|
-
sys.exit(1)
|
46
|
+
if cfg['prompt'] not in prompt:
|
47
|
+
return False
|
42
48
|
|
43
|
-
|
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
|
-
|
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 =
|
92
|
-
|
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
|
-
|
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=
|
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
|
-
|
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
|
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
|
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
|
-
#
|
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
|
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
|
-
#
|
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
|
-
#
|
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/
|
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
|
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.
|