ansible-core 2.19.0b3__py3-none-any.whl → 2.19.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansible/_internal/__init__.py +2 -2
- ansible/_internal/_collection_proxy.py +1 -1
- ansible/_internal/_errors/_alarm_timeout.py +66 -0
- ansible/_internal/_errors/_captured.py +25 -30
- ansible/_internal/_errors/_error_factory.py +89 -0
- ansible/_internal/_errors/_error_utils.py +240 -0
- ansible/_internal/_errors/_task_timeout.py +28 -0
- ansible/_internal/_event_formatting.py +127 -0
- ansible/_internal/_json/__init__.py +6 -6
- ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
- ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
- ansible/_internal/_json/_profiles/_legacy.py +3 -11
- ansible/_internal/_ssh/__init__.py +0 -0
- ansible/_internal/_ssh/_agent_launch.py +91 -0
- ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
- ansible/_internal/_templating/__init__.py +5 -3
- ansible/_internal/_templating/_datatag.py +2 -1
- ansible/_internal/_templating/_engine.py +3 -4
- ansible/_internal/_templating/_jinja_bits.py +21 -16
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +31 -3
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_transform.py +20 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_testing.py +26 -0
- ansible/_internal/_yaml/_dumper.py +1 -1
- ansible/_internal/_yaml/_errors.py +7 -7
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
- ansible/cli/__init__.py +5 -82
- ansible/cli/arguments/option_helpers.py +8 -5
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +27 -23
- ansible/config/manager.py +142 -101
- ansible/constants.py +1 -1
- ansible/errors/__init__.py +96 -49
- ansible/executor/module_common.py +8 -10
- ansible/executor/powershell/async_watchdog.ps1 +2 -2
- ansible/executor/powershell/async_wrapper.ps1 +3 -3
- ansible/executor/powershell/become_wrapper.ps1 +20 -2
- ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
- ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
- ansible/executor/powershell/exec_wrapper.ps1 +219 -6
- ansible/executor/powershell/module_manifest.py +52 -0
- ansible/executor/powershell/module_wrapper.ps1 +47 -21
- ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
- ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
- ansible/executor/process/worker.py +38 -113
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/collection/__init__.py +1 -4
- ansible/inventory/manager.py +1 -0
- ansible/module_utils/_internal/__init__.py +0 -3
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz.py +4 -2
- ansible/module_utils/_internal/_datatag/__init__.py +20 -14
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +66 -48
- ansible/module_utils/_internal/_errors.py +88 -17
- ansible/module_utils/_internal/_event_utils.py +61 -0
- ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
- ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +1 -1
- ansible/module_utils/_internal/_stack.py +22 -0
- ansible/module_utils/_internal/_text_utils.py +6 -0
- ansible/module_utils/_internal/_traceback.py +11 -8
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +49 -15
- ansible/module_utils/common/arg_spec.py +2 -2
- ansible/module_utils/common/collections.py +6 -0
- ansible/module_utils/common/json.py +2 -2
- ansible/module_utils/common/text/converters.py +3 -3
- ansible/module_utils/common/validation.py +1 -1
- ansible/module_utils/common/warnings.py +80 -23
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +15 -22
- ansible/modules/git.py +1 -6
- ansible/modules/hostname.py +0 -1
- ansible/modules/pip.py +2 -4
- ansible/modules/service.py +3 -9
- ansible/modules/sysvinit.py +3 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +4 -4
- ansible/parsing/mod_args.py +1 -1
- ansible/parsing/plugin_docs.py +2 -2
- ansible/parsing/utils/yaml.py +3 -3
- ansible/parsing/vault/__init__.py +4 -4
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +0 -3
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +9 -32
- ansible/plugins/action/add_host.py +1 -1
- ansible/plugins/action/assemble.py +8 -16
- ansible/plugins/action/async_status.py +7 -2
- ansible/plugins/action/copy.py +8 -7
- ansible/plugins/action/gather_facts.py +8 -8
- ansible/plugins/action/package.py +5 -8
- ansible/plugins/action/script.py +8 -15
- ansible/plugins/action/service.py +3 -7
- ansible/plugins/action/template.py +6 -8
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/connection/local.py +2 -2
- ansible/plugins/connection/ssh.py +17 -9
- ansible/plugins/connection/winrm.py +5 -2
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +13 -6
- ansible/plugins/filter/encryption.py +4 -4
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +69 -16
- ansible/plugins/loader.py +10 -9
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +66 -9
- ansible/plugins/shell/sh.py +9 -5
- ansible/plugins/test/core.py +21 -15
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +30 -2
- ansible/utils/collection_loader/__init__.py +2 -0
- ansible/utils/display.py +107 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/docker.txt +3 -3
- ansible_test/_data/completion/remote.txt +1 -0
- ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/util.py +20 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import abc
|
4
4
|
import collections.abc as c
|
5
|
+
import enum
|
5
6
|
import inspect
|
6
7
|
import itertools
|
7
8
|
import typing as t
|
@@ -9,7 +10,7 @@ import typing as t
|
|
9
10
|
from jinja2 import UndefinedError, StrictUndefined, TemplateRuntimeError
|
10
11
|
from jinja2.utils import missing
|
11
12
|
|
12
|
-
from
|
13
|
+
from ...module_utils._internal import _messages
|
13
14
|
from ansible.constants import config
|
14
15
|
from ansible.errors import AnsibleUndefinedVariable, AnsibleTypeError
|
15
16
|
from ansible._internal._errors._handler import ErrorHandler
|
@@ -24,10 +25,16 @@ from ...module_utils.datatag import native_type_name
|
|
24
25
|
_patch_jinja() # apply Jinja2 patches before types are declared that are dependent on the changes
|
25
26
|
|
26
27
|
|
28
|
+
class _SandboxMode(enum.Enum):
|
29
|
+
DEFAULT = enum.auto()
|
30
|
+
ALLOW_UNSAFE_ATTRIBUTES = enum.auto()
|
31
|
+
|
32
|
+
|
27
33
|
class _TemplateConfig:
|
28
34
|
allow_embedded_templates: bool = config.get_config_value("ALLOW_EMBEDDED_TEMPLATES")
|
29
35
|
allow_broken_conditionals: bool = config.get_config_value('ALLOW_BROKEN_CONDITIONALS')
|
30
36
|
jinja_extensions: list[str] = config.get_config_value('DEFAULT_JINJA2_EXTENSIONS')
|
37
|
+
sandbox_mode: _SandboxMode = _SandboxMode.__members__[config.get_config_value('_TEMPLAR_SANDBOX_MODE').upper()]
|
31
38
|
|
32
39
|
unknown_type_encountered_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_ENCOUNTERED')
|
33
40
|
unknown_type_conversion_handler = ErrorHandler.from_config('_TEMPLAR_UNKNOWN_TYPE_CONVERSION')
|
@@ -55,7 +62,7 @@ class Marker(StrictUndefined, Tripwire):
|
|
55
62
|
|
56
63
|
__slots__ = ('_marker_template_source',)
|
57
64
|
|
58
|
-
|
65
|
+
_concrete_subclasses: t.ClassVar[set[type[Marker]]] = set()
|
59
66
|
|
60
67
|
def __init__(
|
61
68
|
self,
|
@@ -129,7 +136,7 @@ class Marker(StrictUndefined, Tripwire):
|
|
129
136
|
def __init_subclass__(cls, **kwargs) -> None:
|
130
137
|
if not inspect.isabstract(cls):
|
131
138
|
_untaggable_types.add(cls)
|
132
|
-
cls.
|
139
|
+
cls._concrete_subclasses.add(cls)
|
133
140
|
|
134
141
|
@classmethod
|
135
142
|
def _init_class(cls):
|
@@ -197,8 +204,6 @@ class TruncationMarker(Marker):
|
|
197
204
|
It will only be visible if the previous `Marker` was ignored/replaced instead of being tripped, which would raise an exception.
|
198
205
|
"""
|
199
206
|
|
200
|
-
# DTFIX-RELEASE: make this a singleton?
|
201
|
-
|
202
207
|
__slots__ = ()
|
203
208
|
|
204
209
|
def __init__(self) -> None:
|
@@ -252,28 +257,18 @@ class UndecryptableVaultError(_captured.AnsibleCapturedError):
|
|
252
257
|
class VaultExceptionMarker(ExceptionMarker):
|
253
258
|
"""A `Marker` value that represents an error accessing a vaulted value during templating."""
|
254
259
|
|
255
|
-
__slots__ = ('_marker_undecryptable_ciphertext', '
|
260
|
+
__slots__ = ('_marker_undecryptable_ciphertext', '_marker_event')
|
256
261
|
|
257
|
-
def __init__(self, ciphertext: str,
|
258
|
-
# DTFIX-RELEASE: when does this show up, should it contain more details?
|
259
|
-
# see also CapturedExceptionMarker for a similar issue
|
262
|
+
def __init__(self, ciphertext: str, event: _messages.Event) -> None:
|
260
263
|
super().__init__(hint='A vault exception marker was tripped.')
|
261
264
|
|
262
265
|
self._marker_undecryptable_ciphertext = ciphertext
|
263
|
-
self.
|
264
|
-
self._marker_undecryptable_traceback = traceback
|
266
|
+
self._marker_event = event
|
265
267
|
|
266
268
|
def _as_exception(self) -> Exception:
|
267
269
|
return UndecryptableVaultError(
|
268
270
|
obj=self._marker_undecryptable_ciphertext,
|
269
|
-
|
270
|
-
details=(
|
271
|
-
Detail(
|
272
|
-
msg=self._marker_undecryptable_reason,
|
273
|
-
),
|
274
|
-
),
|
275
|
-
formatted_traceback=self._marker_undecryptable_traceback,
|
276
|
-
),
|
271
|
+
event=self._marker_event,
|
277
272
|
)
|
278
273
|
|
279
274
|
def _disarm(self) -> str:
|
@@ -282,16 +277,12 @@ class VaultExceptionMarker(ExceptionMarker):
|
|
282
277
|
|
283
278
|
def get_first_marker_arg(args: c.Sequence, kwargs: dict[str, t.Any]) -> Marker | None:
|
284
279
|
"""Utility method to inspect plugin args and return the first `Marker` encountered, otherwise `None`."""
|
285
|
-
#
|
286
|
-
|
287
|
-
return arg
|
288
|
-
|
289
|
-
return None
|
280
|
+
# CAUTION: This function is exposed in public API as ansible.template.get_first_marker_arg.
|
281
|
+
return next(iter_marker_args(args, kwargs), None)
|
290
282
|
|
291
283
|
|
292
284
|
def iter_marker_args(args: c.Sequence, kwargs: dict[str, t.Any]) -> t.Generator[Marker]:
|
293
285
|
"""Utility method to iterate plugin args and yield any `Marker` encountered."""
|
294
|
-
# DTFIX-RELEASE: this may or may not need to be public API, move back to utils or once usage is wrapped in a decorator?
|
295
286
|
for arg in itertools.chain(args, kwargs.values()):
|
296
287
|
if isinstance(arg, Marker):
|
297
288
|
yield arg
|
@@ -306,7 +297,7 @@ class JinjaCallContext(NotifiableAccessContextBase):
|
|
306
297
|
_mask = True
|
307
298
|
|
308
299
|
def __init__(self, accept_lazy_markers: bool) -> None:
|
309
|
-
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker.
|
300
|
+
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker._concrete_subclasses)
|
310
301
|
|
311
302
|
def _notify(self, o: Marker) -> t.NoReturn:
|
312
303
|
o.trip()
|
@@ -314,7 +305,7 @@ class JinjaCallContext(NotifiableAccessContextBase):
|
|
314
305
|
|
315
306
|
def validate_arg_type(name: str, value: t.Any, allowed_type_or_types: type | tuple[type, ...], /) -> None:
|
316
307
|
"""Validate the type of the given argument while preserving context for Marker values."""
|
317
|
-
# DTFIX-
|
308
|
+
# DTFIX-FUTURE: find a home for this as a general-purpose utliity method and expose it after some API review
|
318
309
|
if isinstance(value, allowed_type_or_types):
|
319
310
|
return
|
320
311
|
|
@@ -6,8 +6,12 @@ import collections.abc as c
|
|
6
6
|
import dataclasses
|
7
7
|
import datetime
|
8
8
|
import functools
|
9
|
+
import inspect
|
10
|
+
import re
|
9
11
|
import typing as t
|
10
12
|
|
13
|
+
from jinja2 import defaults
|
14
|
+
|
11
15
|
from ansible.module_utils._internal._ambient_context import AmbientContextBase
|
12
16
|
from ansible.module_utils.common.collections import is_sequence
|
13
17
|
from ansible.module_utils._internal._datatag import AnsibleTagHelper
|
@@ -115,7 +119,7 @@ class JinjaPluginIntercept(c.MutableMapping):
|
|
115
119
|
except MarkerError as ex:
|
116
120
|
return ex.source
|
117
121
|
except Exception as ex:
|
118
|
-
raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-
|
122
|
+
raise AnsibleTemplatePluginRuntimeError(instance.plugin_type, instance.ansible_name) from ex # DTFIX-FUTURE: which name to use? use plugin info?
|
119
123
|
|
120
124
|
def _wrap_test(self, instance: AnsibleJinja2Plugin) -> t.Callable:
|
121
125
|
"""Intercept point for all test plugins to ensure that args are properly templated/lazified."""
|
@@ -127,7 +131,6 @@ class JinjaPluginIntercept(c.MutableMapping):
|
|
127
131
|
if not isinstance(result, bool):
|
128
132
|
template = TemplateContext.current().template_value
|
129
133
|
|
130
|
-
# DTFIX-RELEASE: which name to use? use plugin info?
|
131
134
|
_display.deprecated(
|
132
135
|
msg=f"The test plugin {instance.ansible_name!r} returned a non-boolean result of type {type(result)!r}. "
|
133
136
|
"Test plugins must have a boolean result.",
|
@@ -254,7 +257,7 @@ def _invoke_lookup(*, plugin_name: str, lookup_terms: list, lookup_kwargs: dict[
|
|
254
257
|
except MarkerError as ex:
|
255
258
|
return ex.source
|
256
259
|
except Exception as ex:
|
257
|
-
# DTFIX-
|
260
|
+
# DTFIX-FUTURE: convert this to the new error/warn/ignore context manager
|
258
261
|
if errors == 'warn':
|
259
262
|
_display.error_as_warning(
|
260
263
|
msg=f'An error occurred while running the lookup plugin {plugin_name!r}.',
|
@@ -339,3 +342,28 @@ def _wrap_plugin_output(o: t.Any) -> t.Any:
|
|
339
342
|
o = list(o)
|
340
343
|
|
341
344
|
return _AnsibleLazyTemplateMixin._try_create(o, LazyOptions.SKIP_TEMPLATES)
|
345
|
+
|
346
|
+
|
347
|
+
_PLUGIN_SOURCES = dict(
|
348
|
+
filter=defaults.DEFAULT_FILTERS,
|
349
|
+
test=defaults.DEFAULT_TESTS,
|
350
|
+
)
|
351
|
+
|
352
|
+
|
353
|
+
def _get_builtin_short_description(plugin: object) -> str:
|
354
|
+
"""
|
355
|
+
Make a reasonable effort to break a function docstring down to a single sentence.
|
356
|
+
We can't use the full docstring due to embedded formatting, particularly RST.
|
357
|
+
This isn't intended to be perfect, just good enough until we can write our own docs for these.
|
358
|
+
"""
|
359
|
+
value = re.split(r'(\.|!|\s\(|:\s)', inspect.getdoc(plugin), 1)[0].replace('\n', ' ')
|
360
|
+
|
361
|
+
if value:
|
362
|
+
value += '.'
|
363
|
+
|
364
|
+
return value
|
365
|
+
|
366
|
+
|
367
|
+
def get_jinja_builtin_plugin_descriptions(plugin_type: str) -> dict[str, str]:
|
368
|
+
"""Returns a dictionary of Jinja builtin plugin names and their short descriptions."""
|
369
|
+
return {f'ansible.builtin.{name}': _get_builtin_short_description(plugin) for name, plugin in _PLUGIN_SOURCES[plugin_type].items() if name.isidentifier()}
|
@@ -43,7 +43,7 @@ _KNOWN_TYPES: t.Final[set[type]] = (
|
|
43
43
|
TemplateModule, # example: '{% import "importme.j2" as im %}{{ im | type_debug }}'
|
44
44
|
}
|
45
45
|
| set(PASS_THROUGH_SCALAR_VAR_TYPES)
|
46
|
-
| set(Marker.
|
46
|
+
| set(Marker._concrete_subclasses)
|
47
47
|
)
|
48
48
|
"""
|
49
49
|
These types are known to the templating system.
|
@@ -195,7 +195,7 @@ class _AnsibleLazyTemplateMixin:
|
|
195
195
|
Return an iterable that wraps each of the given elements in a lazy wrapper.
|
196
196
|
Only elements wrapped this way will receive lazy processing when retrieved from the collection.
|
197
197
|
"""
|
198
|
-
# DTFIX-
|
198
|
+
# DTFIX-FUTURE: check relative performance of method-local vs stored generator expressions on implementations of this method
|
199
199
|
raise NotImplementedError() # pragma: nocover
|
200
200
|
|
201
201
|
def _proxy_or_render_lazy_value(self, key: t.Any, value: t.Any) -> t.Any:
|
@@ -346,13 +346,13 @@ class _AnsibleLazyTemplateDict(_AnsibleTaggedDict, _AnsibleLazyTemplateMixin):
|
|
346
346
|
return super().__ne__(other)
|
347
347
|
|
348
348
|
def __or__(self, other):
|
349
|
-
# DTFIX-
|
349
|
+
# DTFIX-FUTURE: support preservation of laziness when possible like we do for list
|
350
350
|
# Both sides end up going through _proxy_or_render_lazy_value, so there's no Templar preservation needed.
|
351
351
|
# In the future this could be made more lazy when both Templar instances are the same, or if per-value Templar tracking was used.
|
352
352
|
return super().__or__(other)
|
353
353
|
|
354
354
|
def __ror__(self, other):
|
355
|
-
# DTFIX-
|
355
|
+
# DTFIX-FUTURE: support preservation of laziness when possible like we do for list
|
356
356
|
# Both sides end up going through _proxy_or_render_lazy_value, so there's no Templar preservation needed.
|
357
357
|
# In the future this could be made more lazy when both Templar instances are the same, or if per-value Templar tracking was used.
|
358
358
|
return super().__ror__(other)
|
@@ -549,7 +549,7 @@ class _AnsibleLazyAccessTuple(_AnsibleTaggedTuple, _AnsibleLazyTemplateMixin):
|
|
549
549
|
created as a results of managed access.
|
550
550
|
"""
|
551
551
|
|
552
|
-
#
|
552
|
+
# DTFIX5: ensure we have tests that explicitly verify this behavior
|
553
553
|
|
554
554
|
# nonempty __slots__ not supported for subtype of 'tuple'
|
555
555
|
|
@@ -5,39 +5,41 @@ from __future__ import annotations
|
|
5
5
|
import dataclasses
|
6
6
|
import typing as t
|
7
7
|
|
8
|
-
from ansible.module_utils._internal import _traceback
|
9
|
-
from ansible.module_utils.common.messages import PluginInfo, ErrorSummary, WarningSummary, DeprecationSummary
|
8
|
+
from ansible.module_utils._internal import _traceback, _event_utils, _messages
|
10
9
|
from ansible.parsing.vault import EncryptedString, VaultHelper
|
11
10
|
from ansible.utils.display import Display
|
12
11
|
|
13
12
|
from ._jinja_common import VaultExceptionMarker
|
14
|
-
from .._errors import _captured,
|
13
|
+
from .._errors import _captured, _error_factory
|
14
|
+
from .. import _event_formatting
|
15
15
|
|
16
16
|
display = Display()
|
17
17
|
|
18
18
|
|
19
|
-
def plugin_info(value: PluginInfo) -> dict[str, str]:
|
19
|
+
def plugin_info(value: _messages.PluginInfo) -> dict[str, str]:
|
20
20
|
"""Render PluginInfo as a dictionary."""
|
21
21
|
return dataclasses.asdict(value)
|
22
22
|
|
23
23
|
|
24
|
-
def error_summary(value: ErrorSummary) -> str:
|
24
|
+
def error_summary(value: _messages.ErrorSummary) -> str:
|
25
25
|
"""Render ErrorSummary as a formatted traceback for backward-compatibility with pre-2.19 TaskResult.exception."""
|
26
|
-
|
26
|
+
if _traceback._is_traceback_enabled(_traceback.TracebackEvent.ERROR):
|
27
|
+
return _event_formatting.format_event_traceback(value.event)
|
27
28
|
|
29
|
+
return '(traceback unavailable)'
|
28
30
|
|
29
|
-
|
31
|
+
|
32
|
+
def warning_summary(value: _messages.WarningSummary) -> str:
|
30
33
|
"""Render WarningSummary as a simple message string for backward-compatibility with pre-2.19 TaskResult.warnings."""
|
31
|
-
return value.
|
34
|
+
return _event_utils.format_event_brief_message(value.event)
|
32
35
|
|
33
36
|
|
34
|
-
def deprecation_summary(value: DeprecationSummary) -> dict[str, t.Any]:
|
37
|
+
def deprecation_summary(value: _messages.DeprecationSummary) -> dict[str, t.Any]:
|
35
38
|
"""Render DeprecationSummary as dict values for backward-compatibility with pre-2.19 TaskResult.deprecations."""
|
36
|
-
|
37
|
-
|
38
|
-
result.pop('details')
|
39
|
+
transformed = _event_utils.deprecation_as_dict(value)
|
40
|
+
transformed.update(deprecator=value.deprecator)
|
39
41
|
|
40
|
-
return
|
42
|
+
return transformed
|
41
43
|
|
42
44
|
|
43
45
|
def encrypted_string(value: EncryptedString) -> str | VaultExceptionMarker:
|
@@ -47,17 +49,16 @@ def encrypted_string(value: EncryptedString) -> str | VaultExceptionMarker:
|
|
47
49
|
except Exception as ex:
|
48
50
|
return VaultExceptionMarker(
|
49
51
|
ciphertext=VaultHelper.get_ciphertext(value, with_tags=True),
|
50
|
-
|
51
|
-
traceback=_traceback.maybe_extract_traceback(ex, _traceback.TracebackEvent.ERROR),
|
52
|
+
event=_error_factory.ControllerEventFactory.from_exception(ex, _traceback.is_traceback_enabled(_traceback.TracebackEvent.ERROR)),
|
52
53
|
)
|
53
54
|
|
54
55
|
|
55
56
|
_type_transform_mapping: dict[type, t.Callable[[t.Any], t.Any]] = {
|
56
57
|
_captured.CapturedErrorSummary: error_summary,
|
57
|
-
PluginInfo: plugin_info,
|
58
|
-
ErrorSummary: error_summary,
|
59
|
-
WarningSummary: warning_summary,
|
60
|
-
DeprecationSummary: deprecation_summary,
|
58
|
+
_messages.PluginInfo: plugin_info,
|
59
|
+
_messages.ErrorSummary: error_summary,
|
60
|
+
_messages.WarningSummary: warning_summary,
|
61
|
+
_messages.DeprecationSummary: deprecation_summary,
|
61
62
|
EncryptedString: encrypted_string,
|
62
63
|
}
|
63
64
|
"""This mapping is consulted by `Templar.template` to provide custom views of some objects."""
|
@@ -99,7 +99,7 @@ Omit = object.__new__(_OmitType)
|
|
99
99
|
_datatag._untaggable_types.add(_OmitType)
|
100
100
|
|
101
101
|
|
102
|
-
#
|
102
|
+
# DTFIX5: review these type sets to ensure they're not overly permissive/dynamic
|
103
103
|
IGNORE_SCALAR_VAR_TYPES = {value for value in _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES if not issubclass(value, str)}
|
104
104
|
|
105
105
|
PASS_THROUGH_SCALAR_VAR_TYPES = _datatag._ANSIBLE_ALLOWED_SCALAR_VAR_TYPES | {
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""
|
2
|
+
Testing utilities for use in integration tests, not unit tests or non-test code.
|
3
|
+
Provides better error behavior than Python's `assert` statement.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import contextlib
|
9
|
+
import typing as t
|
10
|
+
|
11
|
+
|
12
|
+
class _Checker:
|
13
|
+
@staticmethod
|
14
|
+
def check(value: object, msg: str | None = 'Value is not truthy.') -> None:
|
15
|
+
"""Raise an `AssertionError` if the given `value` is not truthy."""
|
16
|
+
if not value:
|
17
|
+
raise AssertionError(msg)
|
18
|
+
|
19
|
+
|
20
|
+
@contextlib.contextmanager
|
21
|
+
def hard_fail_context(msg: str) -> t.Generator[_Checker]:
|
22
|
+
"""Enter a context which converts all exceptions to `BaseException` and provides a `Checker` instance for making assertions."""
|
23
|
+
try:
|
24
|
+
yield _Checker()
|
25
|
+
except BaseException as ex:
|
26
|
+
raise BaseException(f"Hard failure: {msg}") from ex
|
@@ -32,7 +32,7 @@ class _BaseDumper(SafeDumper, metaclass=abc.ABCMeta):
|
|
32
32
|
class AnsibleDumper(_BaseDumper):
|
33
33
|
"""A simple stub class that allows us to add representers for our custom types."""
|
34
34
|
|
35
|
-
#
|
35
|
+
# DTFIX0: need a better way to handle serialization controls during YAML dumping
|
36
36
|
def __init__(self, *args, dump_vault_tags: bool | None = None, **kwargs):
|
37
37
|
super().__init__(*args, **kwargs)
|
38
38
|
|
@@ -7,7 +7,7 @@ import typing as t
|
|
7
7
|
from yaml import MarkedYAMLError
|
8
8
|
from yaml.constructor import ConstructorError
|
9
9
|
|
10
|
-
from ansible._internal._errors import
|
10
|
+
from ansible._internal._errors import _error_utils
|
11
11
|
from ansible.errors import AnsibleParserError
|
12
12
|
from ansible._internal._datatag._tags import Origin
|
13
13
|
|
@@ -34,7 +34,7 @@ class AnsibleYAMLParserError(AnsibleParserError):
|
|
34
34
|
if isinstance(exception, MarkedYAMLError):
|
35
35
|
origin = origin.replace(line_num=exception.problem_mark.line + 1, col_num=exception.problem_mark.column + 1)
|
36
36
|
|
37
|
-
source_context =
|
37
|
+
source_context = _error_utils.SourceContext.from_origin(origin)
|
38
38
|
|
39
39
|
target_line = source_context.target_line or '' # for these cases, we don't need to distinguish between None and empty string
|
40
40
|
|
@@ -66,12 +66,12 @@ class AnsibleYAMLParserError(AnsibleParserError):
|
|
66
66
|
# There may be cases where there is a valid tab in a line that has other errors.
|
67
67
|
# That's OK, users should "fix" their tab usage anyway -- at which point later error handling logic will hopefully find the real issue.
|
68
68
|
elif (tab_idx := target_line.find('\t')) >= 0:
|
69
|
-
source_context =
|
69
|
+
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=tab_idx + 1))
|
70
70
|
message = "Tabs are usually invalid in YAML."
|
71
71
|
|
72
72
|
# Check for unquoted templates.
|
73
73
|
elif match := re.search(r'^\s*(?:-\s+)*(?:[\w\s]+:\s+)?(?P<value>\{\{.*}})', target_line):
|
74
|
-
source_context =
|
74
|
+
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
|
75
75
|
message = 'This may be an issue with missing quotes around a template block.'
|
76
76
|
# FIXME: Use the captured value to show the actual fix required.
|
77
77
|
help_text = """
|
@@ -95,7 +95,7 @@ Should be:
|
|
95
95
|
# look for an unquoted colon in the value
|
96
96
|
and (colon_match := re.search(r':($| )', target_fragment))
|
97
97
|
):
|
98
|
-
source_context =
|
98
|
+
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=value_match.start('value') + colon_match.start() + 1))
|
99
99
|
message = 'Colons in unquoted values must be followed by a non-space character.'
|
100
100
|
# FIXME: Use the captured value to show the actual fix required.
|
101
101
|
help_text = """
|
@@ -114,7 +114,7 @@ Should be:
|
|
114
114
|
first, last = suspected_value[0], suspected_value[-1]
|
115
115
|
|
116
116
|
if first != last: # "foo" in bar
|
117
|
-
source_context =
|
117
|
+
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
|
118
118
|
message = 'Values starting with a quote must end with the same quote.'
|
119
119
|
# FIXME: Use the captured value to show the actual fix required, and use that same logic to improve the origin further.
|
120
120
|
help_text = """
|
@@ -127,7 +127,7 @@ Should be:
|
|
127
127
|
raw: '"foo" in bar'
|
128
128
|
"""
|
129
129
|
elif first == last and target_line.count(first) > 2: # "foo" and "bar"
|
130
|
-
source_context =
|
130
|
+
source_context = _error_utils.SourceContext.from_origin(origin.replace(col_num=match.start('value') + 1))
|
131
131
|
message = 'Values starting with a quote must end with the same quote, and not contain that quote.'
|
132
132
|
# FIXME: Use the captured value to show the actual fix required, and use that same logic to improve the origin further.
|
133
133
|
help_text = """
|
@@ -12,7 +12,7 @@ from ansible.errors import AnsibleError
|
|
12
12
|
|
13
13
|
def unmask(value: object, type_names: str | list[str]) -> object:
|
14
14
|
"""
|
15
|
-
Internal filter to suppress automatic type transformation in Jinja (e.g.,
|
15
|
+
Internal filter to suppress automatic type transformation in Jinja (e.g., WarningSummary, DeprecationSummary, ErrorSummary).
|
16
16
|
Lazy collection caching is in play - the first attempt to access a value in a given lazy container must be with unmasking in place, or the transformed value
|
17
17
|
will already be cached.
|
18
18
|
"""
|
ansible/cli/__init__.py
CHANGED
@@ -7,7 +7,6 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import locale
|
9
9
|
import os
|
10
|
-
import signal
|
11
10
|
import sys
|
12
11
|
|
13
12
|
# We overload the ``ansible`` adhoc command to provide the functionality for
|
@@ -75,8 +74,6 @@ def initialize_locale():
|
|
75
74
|
|
76
75
|
initialize_locale()
|
77
76
|
|
78
|
-
|
79
|
-
import atexit
|
80
77
|
import errno
|
81
78
|
import getpass
|
82
79
|
import subprocess
|
@@ -96,7 +93,7 @@ try:
|
|
96
93
|
display = Display()
|
97
94
|
except Exception as ex:
|
98
95
|
if isinstance(ex, AnsibleError):
|
99
|
-
ex_msg = ' '.join((ex.message, ex._help_text)).strip()
|
96
|
+
ex_msg = ' '.join((ex.message, ex._help_text or '')).strip()
|
100
97
|
else:
|
101
98
|
ex_msg = str(ex)
|
102
99
|
|
@@ -112,17 +109,17 @@ from ansible.module_utils.six import string_types
|
|
112
109
|
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
113
110
|
from ansible.module_utils.common.collections import is_sequence
|
114
111
|
from ansible.module_utils.common.file import is_executable
|
115
|
-
from ansible.module_utils.common.process import get_bin_path
|
116
112
|
from ansible.parsing.dataloader import DataLoader
|
117
113
|
from ansible.parsing.vault import PromptVaultSecret, get_file_vault_secret, VaultSecretsContext
|
118
114
|
from ansible.plugins.loader import add_all_plugin_dirs, init_plugin_loader
|
119
115
|
from ansible.release import __version__
|
120
|
-
from ansible.utils._ssh_agent import SshAgentClient
|
121
116
|
from ansible.utils.collection_loader import AnsibleCollectionConfig
|
122
117
|
from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
|
123
118
|
from ansible.utils.path import unfrackpath
|
124
119
|
from ansible.vars.manager import VariableManager
|
125
120
|
from ansible.module_utils._internal import _deprecator
|
121
|
+
from ansible._internal._ssh import _agent_launch
|
122
|
+
|
126
123
|
|
127
124
|
try:
|
128
125
|
import argcomplete
|
@@ -131,77 +128,6 @@ except ImportError:
|
|
131
128
|
HAS_ARGCOMPLETE = False
|
132
129
|
|
133
130
|
|
134
|
-
_SSH_AGENT_STDOUT_READ_TIMEOUT = 5 # seconds
|
135
|
-
|
136
|
-
|
137
|
-
def _ssh_agent_timeout_handler(signum, frame):
|
138
|
-
raise TimeoutError
|
139
|
-
|
140
|
-
|
141
|
-
def _launch_ssh_agent() -> None:
|
142
|
-
ssh_agent_cfg = C.config.get_config_value('SSH_AGENT')
|
143
|
-
match ssh_agent_cfg:
|
144
|
-
case 'none':
|
145
|
-
display.debug('SSH_AGENT set to none')
|
146
|
-
return
|
147
|
-
case 'auto':
|
148
|
-
try:
|
149
|
-
ssh_agent_bin = get_bin_path('ssh-agent')
|
150
|
-
except ValueError as e:
|
151
|
-
raise AnsibleError('SSH_AGENT set to auto, but cannot find ssh-agent binary') from e
|
152
|
-
ssh_agent_dir = os.path.join(C.DEFAULT_LOCAL_TMP, 'ssh_agent')
|
153
|
-
os.mkdir(ssh_agent_dir, 0o700)
|
154
|
-
sock = os.path.join(ssh_agent_dir, 'agent.sock')
|
155
|
-
display.vvv('SSH_AGENT: starting...')
|
156
|
-
try:
|
157
|
-
p = subprocess.Popen(
|
158
|
-
[ssh_agent_bin, '-D', '-s', '-a', sock],
|
159
|
-
stdin=subprocess.PIPE,
|
160
|
-
stdout=subprocess.PIPE,
|
161
|
-
stderr=subprocess.PIPE,
|
162
|
-
)
|
163
|
-
except OSError as e:
|
164
|
-
raise AnsibleError(
|
165
|
-
f'Could not start ssh-agent: {e}'
|
166
|
-
) from e
|
167
|
-
|
168
|
-
if p.poll() is not None:
|
169
|
-
raise AnsibleError(
|
170
|
-
f'Could not start ssh-agent: rc={p.returncode} stderr="{p.stderr.read().decode()}"'
|
171
|
-
)
|
172
|
-
|
173
|
-
old_sigalrm_handler = signal.signal(signal.SIGALRM, _ssh_agent_timeout_handler)
|
174
|
-
signal.alarm(_SSH_AGENT_STDOUT_READ_TIMEOUT)
|
175
|
-
try:
|
176
|
-
stdout = p.stdout.read(13)
|
177
|
-
except TimeoutError:
|
178
|
-
stdout = b''
|
179
|
-
finally:
|
180
|
-
signal.alarm(0)
|
181
|
-
signal.signal(signal.SIGALRM, old_sigalrm_handler)
|
182
|
-
|
183
|
-
if stdout != b'SSH_AUTH_SOCK':
|
184
|
-
display.warning(
|
185
|
-
f'The first 13 characters of stdout did not match the '
|
186
|
-
f'expected SSH_AUTH_SOCK. This may not be the right binary, '
|
187
|
-
f'or an incompatible agent: {stdout.decode()}'
|
188
|
-
)
|
189
|
-
display.vvv(f'SSH_AGENT: ssh-agent[{p.pid}] started and bound to {sock}')
|
190
|
-
atexit.register(p.terminate)
|
191
|
-
case _:
|
192
|
-
sock = ssh_agent_cfg
|
193
|
-
|
194
|
-
try:
|
195
|
-
with SshAgentClient(sock) as client:
|
196
|
-
client.list()
|
197
|
-
except Exception as e:
|
198
|
-
raise AnsibleError(
|
199
|
-
f'Could not communicate with ssh-agent using auth sock {sock}: {e}'
|
200
|
-
) from e
|
201
|
-
|
202
|
-
os.environ['SSH_AUTH_SOCK'] = os.environ['ANSIBLE_SSH_AGENT'] = sock
|
203
|
-
|
204
|
-
|
205
131
|
class CLI(ABC):
|
206
132
|
""" code behind bin/ansible* programs """
|
207
133
|
|
@@ -636,10 +562,7 @@ class CLI(ABC):
|
|
636
562
|
loader.set_vault_secrets(vault_secrets)
|
637
563
|
|
638
564
|
if self.USES_CONNECTION:
|
639
|
-
|
640
|
-
_launch_ssh_agent()
|
641
|
-
except Exception as e:
|
642
|
-
raise AnsibleError('Failed to launch ssh agent', orig_exc=e)
|
565
|
+
_agent_launch.launch_ssh_agent()
|
643
566
|
|
644
567
|
# create the inventory, and filter it based on the subset specified (if any)
|
645
568
|
inventory = InventoryManager(loader=loader, sources=options['inventory'], cache=(not options.get('flush_cache')))
|
@@ -750,7 +673,7 @@ class CLI(ABC):
|
|
750
673
|
try:
|
751
674
|
raise AnsibleError("Unexpected Exception, this is probably a bug.") from ex
|
752
675
|
except AnsibleError as ex2:
|
753
|
-
# DTFIX-
|
676
|
+
# DTFIX-FUTURE: clean this up so we're not hacking the internals- re-wrap in an AnsibleCLIUnhandledError that always shows TB, or?
|
754
677
|
from ansible.module_utils._internal import _traceback
|
755
678
|
_traceback._is_traceback_enabled = lambda *_args, **_kwargs: True
|
756
679
|
display.error(ex2)
|
@@ -16,10 +16,9 @@ import typing as t
|
|
16
16
|
|
17
17
|
import yaml
|
18
18
|
|
19
|
-
from jinja2 import __version__ as j2_version
|
20
|
-
|
21
19
|
import ansible
|
22
20
|
from ansible import constants as C
|
21
|
+
from ansible._internal import _templating
|
23
22
|
from ansible.module_utils.common.text.converters import to_native
|
24
23
|
from ansible.module_utils.common.yaml import HAS_LIBYAML, yaml_load
|
25
24
|
from ansible.release import __version__
|
@@ -313,7 +312,7 @@ def version(prog=None):
|
|
313
312
|
result.append(" ansible collection location = %s" % ':'.join(C.COLLECTIONS_PATHS))
|
314
313
|
result.append(" executable location = %s" % sys.argv[0])
|
315
314
|
result.append(" python version = %s (%s)" % (''.join(sys.version.splitlines()), to_native(sys.executable)))
|
316
|
-
result.append(" jinja version =
|
315
|
+
result.append(f" jinja version = {_templating.jinja2_version}")
|
317
316
|
result.append(f" pyyaml version = {yaml.__version__} ({libyaml_fragment})")
|
318
317
|
|
319
318
|
return "\n".join(result)
|
@@ -535,13 +534,17 @@ def _tagged_type_factory(name: str, func: t.Callable[[str], object], /) -> t.Cal
|
|
535
534
|
def tag_value(value: str) -> object:
|
536
535
|
result = func(value)
|
537
536
|
|
538
|
-
if result is value:
|
537
|
+
if result is value or func is str:
|
539
538
|
# Values which are not mutated are automatically trusted for templating.
|
540
539
|
# The `is` reference equality is critically important, as other types may only alter the tags, so object equality is
|
541
540
|
# not sufficient to prevent them being tagged as trusted when they should not.
|
541
|
+
# Explicitly include all usages using the `str` type factory since it strips tags.
|
542
542
|
result = TrustedAsTemplate().tag(result)
|
543
543
|
|
544
|
-
|
544
|
+
if not (origin := Origin.get_tag(value)):
|
545
|
+
origin = Origin(description=f'<CLI option {name!r}>')
|
546
|
+
|
547
|
+
return origin.tag(result)
|
545
548
|
|
546
549
|
tag_value._name = name # simplify debugging by attaching the argument name to the function
|
547
550
|
|