ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b6__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 +1 -1
- ansible/_internal/_ansiballz/__init__.py +0 -0
- ansible/_internal/_ansiballz/_builder.py +101 -0
- ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
- 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 +5 -5
- 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 +28 -20
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +36 -5
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_template_vars.py +72 -0
- ansible/_internal/_templating/_transform.py +26 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_yaml/_constructor.py +4 -4
- ansible/_internal/_yaml/_dumper.py +26 -18
- 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 +11 -93
- ansible/cli/arguments/option_helpers.py +3 -4
- ansible/cli/console.py +1 -1
- ansible/cli/doc.py +86 -30
- ansible/cli/inventory.py +5 -7
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +46 -0
- ansible/errors/__init__.py +98 -50
- ansible/executor/module_common.py +75 -49
- 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 +40 -115
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/api.py +1 -4
- ansible/galaxy/collection/__init__.py +2 -10
- ansible/galaxy/collection/concrete_artifact_manager.py +2 -8
- ansible/galaxy/role.py +2 -2
- ansible/inventory/manager.py +1 -1
- ansible/module_utils/_internal/__init__.py +7 -7
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
- ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +13 -39
- ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
- ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
- ansible/module_utils/_internal/_datatag/__init__.py +43 -15
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +67 -55
- 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 +22 -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} +54 -49
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +15 -2
- 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 +95 -71
- 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/respawn.py +4 -41
- 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/connection.py +8 -11
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/hardware/linux.py +1 -1
- ansible/module_utils/facts/sysctl.py +4 -6
- ansible/module_utils/facts/system/caps.py +2 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/system/local.py +1 -1
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/service.py +3 -10
- ansible/module_utils/urls.py +4 -4
- ansible/modules/apt_repository.py +17 -39
- ansible/modules/assemble.py +2 -2
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +12 -22
- ansible/modules/command.py +3 -3
- ansible/modules/copy.py +4 -4
- ansible/modules/cron.py +1 -1
- ansible/modules/dnf5.py +14 -22
- ansible/modules/file.py +16 -17
- ansible/modules/find.py +3 -3
- ansible/modules/get_url.py +17 -0
- ansible/modules/git.py +9 -7
- ansible/modules/hostname.py +0 -1
- ansible/modules/known_hosts.py +12 -14
- ansible/modules/package.py +6 -0
- ansible/modules/replace.py +2 -2
- ansible/modules/service.py +3 -9
- ansible/modules/slurp.py +10 -13
- ansible/modules/stat.py +5 -7
- ansible/modules/unarchive.py +6 -6
- ansible/modules/user.py +1 -1
- ansible/modules/wait_for.py +28 -30
- ansible/modules/yum_repository.py +4 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +6 -6
- 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 +10 -14
- ansible/playbook/base.py +7 -2
- ansible/playbook/included_file.py +3 -1
- ansible/playbook/play_context.py +2 -0
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +19 -8
- ansible/playbook/task.py +2 -0
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +8 -31
- 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/fetch.py +3 -3
- 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 +11 -10
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/cache/__init__.py +17 -19
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/callback/tree.py +5 -5
- ansible/plugins/connection/local.py +6 -6
- ansible/plugins/connection/paramiko_ssh.py +5 -5
- ansible/plugins/connection/ssh.py +25 -15
- ansible/plugins/connection/winrm.py +6 -3
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +32 -27
- ansible/plugins/filter/encryption.py +14 -6
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +73 -19
- ansible/plugins/loader.py +7 -7
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/lookup/template.py +9 -4
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +67 -9
- ansible/plugins/shell/sh.py +10 -5
- ansible/plugins/strategy/__init__.py +3 -3
- ansible/plugins/test/core.py +22 -16
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +38 -54
- ansible/utils/collection_loader/_collection_finder.py +3 -3
- ansible/utils/display.py +124 -138
- ansible/utils/galaxy.py +2 -2
- ansible/utils/hashing.py +6 -8
- ansible/utils/listify.py +6 -4
- ansible/utils/path.py +5 -7
- ansible/utils/py3compat.py +2 -1
- ansible/utils/ssh_functions.py +3 -2
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- ansible/vars/plugins.py +3 -3
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/RECORD +224 -204
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.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/commands/integration/coverage.py +7 -2
- ansible_test/_internal/host_profiles.py +62 -10
- ansible_test/_internal/provisioning.py +10 -4
- ansible_test/_internal/ssh.py +1 -5
- ansible_test/_internal/thread.py +2 -1
- ansible_test/_internal/timeout.py +1 -1
- ansible_test/_internal/util.py +40 -12
- 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 +61 -7
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible_test/_util/target/setup/requirements.py +3 -9
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,240 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import abc
|
4
|
+
import collections.abc as _c
|
5
|
+
import dataclasses
|
6
|
+
import itertools
|
7
|
+
import pathlib
|
8
|
+
import textwrap
|
9
|
+
import typing as t
|
10
|
+
|
11
|
+
from ansible._internal._datatag._tags import Origin
|
12
|
+
from ansible._internal._errors import _error_factory
|
13
|
+
from ansible.module_utils._internal import _ambient_context, _event_utils, _messages, _traceback
|
14
|
+
|
15
|
+
|
16
|
+
class ContributesToTaskResult(metaclass=abc.ABCMeta):
|
17
|
+
"""Exceptions may include this mixin to contribute task result dictionary data directly to the final result."""
|
18
|
+
|
19
|
+
@property
|
20
|
+
@abc.abstractmethod
|
21
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
22
|
+
"""Mapping of results to apply to the task result."""
|
23
|
+
|
24
|
+
@property
|
25
|
+
def omit_exception_key(self) -> bool:
|
26
|
+
"""Non-error exceptions (e.g., `AnsibleActionSkip`) must return `True` to ensure omission of the `exception` key."""
|
27
|
+
return False
|
28
|
+
|
29
|
+
@property
|
30
|
+
def omit_failed_key(self) -> bool:
|
31
|
+
"""Exceptions representing non-failure scenarios (e.g., `skipped`, `unreachable`) must return `True` to ensure omisson of the `failed` key."""
|
32
|
+
return False
|
33
|
+
|
34
|
+
|
35
|
+
class RedactAnnotatedSourceContext(_ambient_context.AmbientContextBase):
|
36
|
+
"""When active, this context will redact annotated source lines, showing only the origin."""
|
37
|
+
|
38
|
+
|
39
|
+
@dataclasses.dataclass(kw_only=True, frozen=True)
|
40
|
+
class SourceContext:
|
41
|
+
origin: Origin
|
42
|
+
annotated_source_lines: list[str]
|
43
|
+
target_line: str | None
|
44
|
+
|
45
|
+
def __str__(self) -> str:
|
46
|
+
msg_lines = [f'Origin: {self.origin}']
|
47
|
+
|
48
|
+
if self.annotated_source_lines:
|
49
|
+
msg_lines.append('')
|
50
|
+
msg_lines.extend(self.annotated_source_lines)
|
51
|
+
|
52
|
+
return '\n'.join(msg_lines)
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def from_value(cls, value: t.Any) -> SourceContext | None:
|
56
|
+
"""Attempt to retrieve source and render a contextual indicator from the value's origin (if any)."""
|
57
|
+
if value is None:
|
58
|
+
return None
|
59
|
+
|
60
|
+
if isinstance(value, Origin):
|
61
|
+
origin = value
|
62
|
+
value = None
|
63
|
+
else:
|
64
|
+
origin = Origin.get_tag(value)
|
65
|
+
|
66
|
+
if RedactAnnotatedSourceContext.current(optional=True):
|
67
|
+
return cls.error('content redacted')
|
68
|
+
|
69
|
+
if origin and origin.path:
|
70
|
+
return cls.from_origin(origin)
|
71
|
+
|
72
|
+
if value is None:
|
73
|
+
truncated_value = None
|
74
|
+
annotated_source_lines = []
|
75
|
+
else:
|
76
|
+
# DTFIX-FUTURE: cleanup/share width
|
77
|
+
try:
|
78
|
+
value = str(value)
|
79
|
+
except Exception as ex:
|
80
|
+
value = f'<< context unavailable: {ex} >>'
|
81
|
+
|
82
|
+
truncated_value = textwrap.shorten(value, width=120)
|
83
|
+
annotated_source_lines = [truncated_value]
|
84
|
+
|
85
|
+
return SourceContext(
|
86
|
+
origin=origin or Origin.UNKNOWN,
|
87
|
+
annotated_source_lines=annotated_source_lines,
|
88
|
+
target_line=truncated_value,
|
89
|
+
)
|
90
|
+
|
91
|
+
@staticmethod
|
92
|
+
def error(message: str | None, origin: Origin | None = None) -> SourceContext:
|
93
|
+
return SourceContext(
|
94
|
+
origin=origin,
|
95
|
+
annotated_source_lines=[f'(source not shown: {message})'] if message else [],
|
96
|
+
target_line=None,
|
97
|
+
)
|
98
|
+
|
99
|
+
@classmethod
|
100
|
+
def from_origin(cls, origin: Origin) -> SourceContext:
|
101
|
+
"""Attempt to retrieve source and render a contextual indicator of an error location."""
|
102
|
+
from ansible.parsing.vault import is_encrypted # avoid circular import
|
103
|
+
|
104
|
+
# DTFIX-FUTURE: support referencing the column after the end of the target line, so we can indicate where a missing character (quote) needs to be added
|
105
|
+
# this is also useful for cases like end-of-stream reported by the YAML parser
|
106
|
+
|
107
|
+
# DTFIX-FUTURE: Implement line wrapping and match annotated line width to the terminal display width.
|
108
|
+
|
109
|
+
context_line_count: t.Final = 2
|
110
|
+
max_annotated_line_width: t.Final = 120
|
111
|
+
truncation_marker: t.Final = '...'
|
112
|
+
|
113
|
+
target_line_num = origin.line_num
|
114
|
+
|
115
|
+
if RedactAnnotatedSourceContext.current(optional=True):
|
116
|
+
return cls.error('content redacted', origin)
|
117
|
+
|
118
|
+
if not target_line_num or target_line_num < 1:
|
119
|
+
return cls.error(None, origin) # message omitted since lack of line number is obvious from pos
|
120
|
+
|
121
|
+
start_line_idx = max(0, (target_line_num - 1) - context_line_count) # if near start of file
|
122
|
+
target_col_num = origin.col_num
|
123
|
+
|
124
|
+
try:
|
125
|
+
with pathlib.Path(origin.path).open() as src:
|
126
|
+
first_line = src.readline()
|
127
|
+
lines = list(itertools.islice(itertools.chain((first_line,), src), start_line_idx, target_line_num))
|
128
|
+
except Exception as ex:
|
129
|
+
return cls.error(type(ex).__name__, origin)
|
130
|
+
|
131
|
+
if is_encrypted(first_line):
|
132
|
+
return cls.error('content encrypted', origin)
|
133
|
+
|
134
|
+
if len(lines) != target_line_num - start_line_idx:
|
135
|
+
return cls.error('file truncated', origin)
|
136
|
+
|
137
|
+
annotated_source_lines = []
|
138
|
+
|
139
|
+
line_label_width = len(str(target_line_num))
|
140
|
+
max_src_line_len = max_annotated_line_width - line_label_width - 1
|
141
|
+
|
142
|
+
usable_line_len = max_src_line_len
|
143
|
+
|
144
|
+
for line_num, line in enumerate(lines, start_line_idx + 1):
|
145
|
+
line = line.rstrip('\n') # universal newline default mode on `open` ensures we'll never see anything but \n
|
146
|
+
line = line.replace('\t', ' ') # mixed tab/space handling is intentionally disabled since we're both format and display config agnostic
|
147
|
+
|
148
|
+
if len(line) > max_src_line_len:
|
149
|
+
line = line[: max_src_line_len - len(truncation_marker)] + truncation_marker
|
150
|
+
usable_line_len = max_src_line_len - len(truncation_marker)
|
151
|
+
|
152
|
+
annotated_source_lines.append(f'{str(line_num).rjust(line_label_width)}{" " if line else ""}{line}')
|
153
|
+
|
154
|
+
if target_col_num and usable_line_len >= target_col_num >= 1:
|
155
|
+
column_marker = f'column {target_col_num}'
|
156
|
+
|
157
|
+
target_col_idx = target_col_num - 1
|
158
|
+
|
159
|
+
if target_col_idx + 2 + len(column_marker) > max_src_line_len:
|
160
|
+
column_marker = f'{" " * (target_col_idx - len(column_marker) - 1)}{column_marker} ^'
|
161
|
+
else:
|
162
|
+
column_marker = f'{" " * target_col_idx}^ {column_marker}'
|
163
|
+
|
164
|
+
column_marker = f'{" " * line_label_width} {column_marker}'
|
165
|
+
|
166
|
+
annotated_source_lines.append(column_marker)
|
167
|
+
elif target_col_num is None:
|
168
|
+
underline_length = len(annotated_source_lines[-1]) - line_label_width - 1
|
169
|
+
annotated_source_lines.append(f'{" " * line_label_width} {"^" * underline_length}')
|
170
|
+
|
171
|
+
return SourceContext(
|
172
|
+
origin=origin,
|
173
|
+
annotated_source_lines=annotated_source_lines,
|
174
|
+
target_line=lines[-1].rstrip('\n'), # universal newline default mode on `open` ensures we'll never see anything but \n
|
175
|
+
)
|
176
|
+
|
177
|
+
|
178
|
+
def format_exception_message(exception: BaseException) -> str:
|
179
|
+
"""Return the full chain of exception messages by concatenating the cause(s) until all are exhausted."""
|
180
|
+
return _event_utils.format_event_brief_message(_error_factory.ControllerEventFactory.from_exception(exception, False))
|
181
|
+
|
182
|
+
|
183
|
+
def result_dict_from_exception(exception: BaseException, accept_result_contribution: bool = False) -> dict[str, object]:
|
184
|
+
"""Return a failed task result dict from the given exception."""
|
185
|
+
event = _error_factory.ControllerEventFactory.from_exception(exception, _traceback.is_traceback_enabled(_traceback.TracebackEvent.ERROR))
|
186
|
+
|
187
|
+
result: dict[str, object] = {}
|
188
|
+
omit_failed_key = False
|
189
|
+
omit_exception_key = False
|
190
|
+
|
191
|
+
if accept_result_contribution:
|
192
|
+
while exception:
|
193
|
+
if isinstance(exception, ContributesToTaskResult):
|
194
|
+
result = dict(exception.result_contribution)
|
195
|
+
omit_failed_key = exception.omit_failed_key
|
196
|
+
omit_exception_key = exception.omit_exception_key
|
197
|
+
break
|
198
|
+
|
199
|
+
exception = exception.__cause__
|
200
|
+
|
201
|
+
if omit_failed_key:
|
202
|
+
result.pop('failed', None)
|
203
|
+
else:
|
204
|
+
result.update(failed=True)
|
205
|
+
|
206
|
+
if omit_exception_key:
|
207
|
+
result.pop('exception', None)
|
208
|
+
else:
|
209
|
+
result.update(exception=_messages.ErrorSummary(event=event))
|
210
|
+
|
211
|
+
if 'msg' not in result:
|
212
|
+
# if nothing contributed `msg`, generate one from the exception messages
|
213
|
+
result.update(msg=_event_utils.format_event_brief_message(event))
|
214
|
+
|
215
|
+
return result
|
216
|
+
|
217
|
+
|
218
|
+
def result_dict_from_captured_errors(
|
219
|
+
msg: str,
|
220
|
+
*,
|
221
|
+
errors: list[_messages.ErrorSummary] | None = None,
|
222
|
+
) -> dict[str, object]:
|
223
|
+
"""Return a failed task result dict from the given error message and captured errors."""
|
224
|
+
_skip_stackwalk = True
|
225
|
+
|
226
|
+
event = _messages.Event(
|
227
|
+
msg=msg,
|
228
|
+
formatted_traceback=_traceback.maybe_capture_traceback(msg, _traceback.TracebackEvent.ERROR),
|
229
|
+
events=tuple(error.event for error in errors) if errors else None,
|
230
|
+
)
|
231
|
+
|
232
|
+
result = dict(
|
233
|
+
failed=True,
|
234
|
+
exception=_messages.ErrorSummary(
|
235
|
+
event=event,
|
236
|
+
),
|
237
|
+
msg=_event_utils.format_event_brief_message(event),
|
238
|
+
)
|
239
|
+
|
240
|
+
return result
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import abc as _c
|
4
|
+
|
5
|
+
from ansible._internal._errors._alarm_timeout import AnsibleTimeoutError
|
6
|
+
from ansible._internal._errors._error_utils import ContributesToTaskResult
|
7
|
+
from ansible.module_utils.datatag import deprecate_value
|
8
|
+
|
9
|
+
|
10
|
+
class TaskTimeoutError(AnsibleTimeoutError, ContributesToTaskResult):
|
11
|
+
"""
|
12
|
+
A task-specific timeout.
|
13
|
+
|
14
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
15
|
+
"""
|
16
|
+
|
17
|
+
@property
|
18
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
19
|
+
help_text = "Configure `DISPLAY_TRACEBACK` to see a traceback on timeout errors."
|
20
|
+
|
21
|
+
frame = deprecate_value(
|
22
|
+
value=help_text,
|
23
|
+
msg="The `timedout.frame` task result key is deprecated.",
|
24
|
+
help_text=help_text,
|
25
|
+
version="2.23",
|
26
|
+
)
|
27
|
+
|
28
|
+
return dict(timedout=dict(frame=frame, period=self.timeout))
|
@@ -0,0 +1,127 @@
|
|
1
|
+
from __future__ import annotations as _annotations
|
2
|
+
|
3
|
+
import collections.abc as _c
|
4
|
+
import textwrap as _textwrap
|
5
|
+
|
6
|
+
from ansible.module_utils._internal import _event_utils, _messages
|
7
|
+
|
8
|
+
|
9
|
+
def format_event(event: _messages.Event, include_traceback: bool) -> str:
|
10
|
+
"""Format an event into a verbose message and traceback."""
|
11
|
+
msg = format_event_verbose_message(event)
|
12
|
+
|
13
|
+
if include_traceback:
|
14
|
+
msg += '\n' + format_event_traceback(event)
|
15
|
+
|
16
|
+
msg = msg.strip()
|
17
|
+
|
18
|
+
if '\n' in msg:
|
19
|
+
msg += '\n\n'
|
20
|
+
else:
|
21
|
+
msg += '\n'
|
22
|
+
|
23
|
+
return msg
|
24
|
+
|
25
|
+
|
26
|
+
def format_event_traceback(event: _messages.Event) -> str:
|
27
|
+
"""Format an event into a traceback."""
|
28
|
+
segments: list[str] = []
|
29
|
+
|
30
|
+
while event:
|
31
|
+
segment = event.formatted_traceback or '(traceback missing)\n'
|
32
|
+
|
33
|
+
if event.events:
|
34
|
+
child_tracebacks = [format_event_traceback(child) for child in event.events]
|
35
|
+
segment += _format_event_children("Sub-Traceback", child_tracebacks)
|
36
|
+
|
37
|
+
segments.append(segment)
|
38
|
+
|
39
|
+
if event.chain:
|
40
|
+
segments.append(f'\n{event.chain.traceback_reason}\n\n')
|
41
|
+
|
42
|
+
event = event.chain.event
|
43
|
+
else:
|
44
|
+
event = None
|
45
|
+
|
46
|
+
return ''.join(reversed(segments))
|
47
|
+
|
48
|
+
|
49
|
+
def format_event_verbose_message(event: _messages.Event) -> str:
|
50
|
+
"""
|
51
|
+
Format an event into a verbose message.
|
52
|
+
Help text, contextual information and sub-events will be included.
|
53
|
+
"""
|
54
|
+
segments: list[str] = []
|
55
|
+
original_event = event
|
56
|
+
|
57
|
+
while event:
|
58
|
+
messages = [event.msg]
|
59
|
+
chain: _messages.EventChain = event.chain
|
60
|
+
|
61
|
+
while chain and chain.follow:
|
62
|
+
if chain.event.events:
|
63
|
+
break # do not collapse a chained event with sub-events, since they would be lost
|
64
|
+
|
65
|
+
if chain.event.formatted_source_context or chain.event.help_text:
|
66
|
+
if chain.event.formatted_source_context != event.formatted_source_context or chain.event.help_text != event.help_text:
|
67
|
+
break # do not collapse a chained event with different details, since they would be lost
|
68
|
+
|
69
|
+
if chain.event.chain and chain.msg_reason != chain.event.chain.msg_reason:
|
70
|
+
break # do not collapse a chained event which has a chain with a different msg_reason
|
71
|
+
|
72
|
+
messages.append(chain.event.msg)
|
73
|
+
|
74
|
+
chain = chain.event.chain
|
75
|
+
|
76
|
+
msg = _event_utils.deduplicate_message_parts(messages)
|
77
|
+
segment = '\n'.join(_get_message_lines(msg, event.help_text, event.formatted_source_context)) + '\n'
|
78
|
+
|
79
|
+
if event.events:
|
80
|
+
child_msgs = [format_event_verbose_message(child) for child in event.events]
|
81
|
+
segment += _format_event_children("Sub-Event", child_msgs)
|
82
|
+
|
83
|
+
segments.append(segment)
|
84
|
+
|
85
|
+
if chain and chain.follow:
|
86
|
+
segments.append(f'\n{chain.msg_reason}\n\n')
|
87
|
+
|
88
|
+
event = chain.event
|
89
|
+
else:
|
90
|
+
event = None
|
91
|
+
|
92
|
+
if len(segments) > 1:
|
93
|
+
segments.insert(0, _event_utils.format_event_brief_message(original_event) + '\n\n')
|
94
|
+
|
95
|
+
return ''.join(segments)
|
96
|
+
|
97
|
+
|
98
|
+
def _format_event_children(label: str, children: _c.Iterable[str]) -> str:
|
99
|
+
"""Format the given list of child messages into a single string."""
|
100
|
+
items = list(children)
|
101
|
+
count = len(items)
|
102
|
+
lines = ['\n']
|
103
|
+
|
104
|
+
for idx, item in enumerate(items):
|
105
|
+
lines.append(f'+--[ {label} {idx + 1} of {count} ]---\n')
|
106
|
+
lines.append(_textwrap.indent(f"\n{item}\n", "| ", lambda value: True))
|
107
|
+
|
108
|
+
lines.append(f'+--[ End {label} ]---\n')
|
109
|
+
|
110
|
+
return ''.join(lines)
|
111
|
+
|
112
|
+
|
113
|
+
def _get_message_lines(message: str, help_text: str | None, formatted_source_context: str | None) -> list[str]:
|
114
|
+
"""Return a list of message lines constructed from the given message, help text and formatted source context."""
|
115
|
+
if help_text and not formatted_source_context and '\n' not in message and '\n' not in help_text:
|
116
|
+
return [f'{message} {help_text}'] # prefer a single-line message with help text when there is no source context
|
117
|
+
|
118
|
+
message_lines = [message]
|
119
|
+
|
120
|
+
if formatted_source_context:
|
121
|
+
message_lines.append(formatted_source_context)
|
122
|
+
|
123
|
+
if help_text:
|
124
|
+
message_lines.append('')
|
125
|
+
message_lines.append(help_text)
|
126
|
+
|
127
|
+
return message_lines
|
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Internal utilities for serialization and deserialization."""
|
2
2
|
|
3
|
-
# DTFIX-
|
3
|
+
# DTFIX-FUTURE: most of this isn't JSON specific, find a better home
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
@@ -144,20 +144,20 @@ class AnsibleVariableVisitor:
|
|
144
144
|
value = self._template_engine.transform(value)
|
145
145
|
value_type = type(value)
|
146
146
|
|
147
|
-
#
|
147
|
+
# DTFIX3: need to handle native copy for keys too
|
148
148
|
if self.convert_to_native_values and isinstance(value, _datatag.AnsibleTaggedObject):
|
149
149
|
value = value._native_copy()
|
150
150
|
value_type = type(value)
|
151
151
|
|
152
152
|
result: _T
|
153
153
|
|
154
|
-
#
|
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
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
|
-
#
|
156
|
+
# DTFIX5: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
|
157
157
|
|
158
158
|
if (result := self._early_visit(value, value_type)) is not _sentinel:
|
159
159
|
pass
|
160
|
-
#
|
160
|
+
# DTFIX7: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
|
161
161
|
elif value_type in _ANSIBLE_ALLOWED_MAPPING_VAR_TYPES: # check mappings first, because they're also collections
|
162
162
|
with self: # supports StateTrackingMixIn
|
163
163
|
result = AnsibleTagHelper.tag_copy(value, ((k, self._visit(k, v)) for k, v in value.items()), value_type=value_type)
|
@@ -46,6 +46,8 @@ class _Profile(_profiles._JSONSerializationProfile):
|
|
46
46
|
_datetime.datetime: _datatag.AnsibleSerializableDateTime,
|
47
47
|
}
|
48
48
|
|
49
|
+
cls.handle_key = cls._handle_key_str_fallback # legacy stdlib-compatible key behavior
|
50
|
+
|
49
51
|
|
50
52
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
51
53
|
_profile = _Profile
|
@@ -12,7 +12,7 @@ from . import _legacy
|
|
12
12
|
class _InventoryVariableVisitor(_legacy._LegacyVariableVisitor, _json.StateTrackingMixIn):
|
13
13
|
"""State-tracking visitor implementation that only applies trust to `_meta.hostvars` and `vars` inventory values."""
|
14
14
|
|
15
|
-
#
|
15
|
+
# DTFIX5: does the variable visitor need to support conversion of sequence/mapping for inventory?
|
16
16
|
|
17
17
|
@property
|
18
18
|
def _allow_trust(self) -> bool:
|
@@ -152,9 +152,11 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
152
152
|
'__ansible_vault': cls.deserialize_vault,
|
153
153
|
}
|
154
154
|
|
155
|
+
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
|
156
|
+
|
155
157
|
@classmethod
|
156
158
|
def pre_serialize(cls, encoder: Encoder, o: _t.Any) -> _t.Any:
|
157
|
-
#
|
159
|
+
# DTFIX7: these conversion args probably aren't needed
|
158
160
|
avv = cls.visitor_type(invert_trust=True, convert_mapping_to_dict=True, convert_sequence_to_list=True, convert_custom_scalars=True)
|
159
161
|
|
160
162
|
return avv.visit(o)
|
@@ -165,16 +167,6 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
165
167
|
|
166
168
|
return avv.visit(o)
|
167
169
|
|
168
|
-
@classmethod
|
169
|
-
def handle_key(cls, k: _t.Any) -> _t.Any:
|
170
|
-
if isinstance(k, str):
|
171
|
-
return k
|
172
|
-
|
173
|
-
# DTFIX-RELEASE: decide if this is a deprecation warning, error, or what?
|
174
|
-
# Non-string variable names have been disallowed by set_fact and other things since at least 2021.
|
175
|
-
# DTFIX-RELEASE: document why this behavior is here, also verify the legacy tagless use case doesn't need this same behavior
|
176
|
-
return str(k)
|
177
|
-
|
178
170
|
|
179
171
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
180
172
|
_profile = _Profile
|
File without changes
|
@@ -0,0 +1,91 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import atexit
|
4
|
+
import os
|
5
|
+
import subprocess
|
6
|
+
|
7
|
+
from ansible import constants as C
|
8
|
+
from ansible._internal._errors import _alarm_timeout
|
9
|
+
from ansible._internal._ssh._ssh_agent import SshAgentClient
|
10
|
+
from ansible.cli import display
|
11
|
+
from ansible.errors import AnsibleError
|
12
|
+
from ansible.module_utils.common.process import get_bin_path
|
13
|
+
|
14
|
+
_SSH_AGENT_STDOUT_READ_TIMEOUT = 5 # seconds
|
15
|
+
|
16
|
+
|
17
|
+
def launch_ssh_agent() -> None:
|
18
|
+
"""If configured via `SSH_AGENT`, launch an ssh-agent for Ansible's use and/or verify access to an existing one."""
|
19
|
+
try:
|
20
|
+
_launch_ssh_agent()
|
21
|
+
except Exception as ex:
|
22
|
+
raise AnsibleError("Failed to launch ssh agent.") from ex
|
23
|
+
|
24
|
+
|
25
|
+
def _launch_ssh_agent() -> None:
|
26
|
+
ssh_agent_cfg = C.config.get_config_value('SSH_AGENT')
|
27
|
+
|
28
|
+
match ssh_agent_cfg:
|
29
|
+
case 'none':
|
30
|
+
display.debug('SSH_AGENT set to none')
|
31
|
+
return
|
32
|
+
case 'auto':
|
33
|
+
try:
|
34
|
+
ssh_agent_bin = get_bin_path(C.config.get_config_value('SSH_AGENT_EXECUTABLE'))
|
35
|
+
except ValueError as e:
|
36
|
+
raise AnsibleError('SSH_AGENT set to auto, but cannot find ssh-agent binary.') from e
|
37
|
+
|
38
|
+
ssh_agent_dir = os.path.join(C.DEFAULT_LOCAL_TMP, 'ssh_agent')
|
39
|
+
os.mkdir(ssh_agent_dir, 0o700)
|
40
|
+
sock = os.path.join(ssh_agent_dir, 'agent.sock')
|
41
|
+
display.vvv('SSH_AGENT: starting...')
|
42
|
+
|
43
|
+
try:
|
44
|
+
p = subprocess.Popen(
|
45
|
+
[ssh_agent_bin, '-D', '-s', '-a', sock],
|
46
|
+
stdin=subprocess.PIPE,
|
47
|
+
stdout=subprocess.PIPE,
|
48
|
+
stderr=subprocess.PIPE,
|
49
|
+
text=True,
|
50
|
+
)
|
51
|
+
except OSError as e:
|
52
|
+
raise AnsibleError('Could not start ssh-agent.') from e
|
53
|
+
|
54
|
+
atexit.register(p.terminate)
|
55
|
+
|
56
|
+
help_text = f'The ssh-agent {ssh_agent_bin!r} might be an incompatible agent.'
|
57
|
+
expected_stdout = 'SSH_AUTH_SOCK'
|
58
|
+
|
59
|
+
try:
|
60
|
+
with _alarm_timeout.AnsibleTimeoutError.alarm_timeout(_SSH_AGENT_STDOUT_READ_TIMEOUT):
|
61
|
+
stdout = p.stdout.read(len(expected_stdout))
|
62
|
+
except _alarm_timeout.AnsibleTimeoutError as e:
|
63
|
+
display.error_as_warning(
|
64
|
+
msg=f'Timed out waiting for expected stdout {expected_stdout!r} from ssh-agent.',
|
65
|
+
exception=e,
|
66
|
+
help_text=help_text,
|
67
|
+
)
|
68
|
+
else:
|
69
|
+
if stdout != expected_stdout:
|
70
|
+
display.warning(
|
71
|
+
msg=f'The ssh-agent output {stdout!r} did not match expected {expected_stdout!r}.',
|
72
|
+
help_text=help_text,
|
73
|
+
)
|
74
|
+
|
75
|
+
if p.poll() is not None:
|
76
|
+
raise AnsibleError(
|
77
|
+
message='The ssh-agent terminated prematurely.',
|
78
|
+
help_text=f'{help_text}\n\nReturn Code: {p.returncode}\nStandard Error:\n{p.stderr.read()}',
|
79
|
+
)
|
80
|
+
|
81
|
+
display.vvv(f'SSH_AGENT: ssh-agent[{p.pid}] started and bound to {sock}')
|
82
|
+
case _:
|
83
|
+
sock = ssh_agent_cfg
|
84
|
+
|
85
|
+
try:
|
86
|
+
with SshAgentClient(sock) as client:
|
87
|
+
client.list()
|
88
|
+
except Exception as e:
|
89
|
+
raise AnsibleError(f'Could not communicate with ssh-agent using auth sock {sock!r}.') from e
|
90
|
+
|
91
|
+
os.environ['SSH_AUTH_SOCK'] = os.environ['ANSIBLE_SSH_AGENT'] = sock
|