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
ansible/_internal/__init__.py
CHANGED
@@ -45,7 +45,7 @@ def setup() -> None:
|
|
45
45
|
"""No-op function to ensure that side-effect only imports of this module are not flagged/removed as 'unused'."""
|
46
46
|
|
47
47
|
|
48
|
-
# DTFIX-
|
48
|
+
# DTFIX-FUTURE: this is really fragile- disordered/incorrect imports (among other things) can mess it up. Consider a hosting-env-managed context
|
49
49
|
# with an enum with at least Controller/Target/Unknown values, and possibly using lazy-init module shims or some other mechanism to allow controller-side
|
50
50
|
# notification/augmentation of this kind of metadata.
|
51
51
|
_internal.get_controller_serialize_map = get_controller_serialize_map
|
File without changes
|
@@ -0,0 +1,101 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import dataclasses
|
4
|
+
import json
|
5
|
+
|
6
|
+
import typing as t
|
7
|
+
|
8
|
+
from ansible.module_utils._internal._ansiballz import _extensions
|
9
|
+
from ansible.module_utils._internal._ansiballz._extensions import _pydevd, _coverage
|
10
|
+
from ansible.constants import config
|
11
|
+
|
12
|
+
_T = t.TypeVar('_T')
|
13
|
+
|
14
|
+
|
15
|
+
class ExtensionManager:
|
16
|
+
"""AnsiballZ extension manager."""
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
debugger: _pydevd.Options | None = None,
|
21
|
+
coverage: _coverage.Options | None = None,
|
22
|
+
) -> None:
|
23
|
+
options = dict(
|
24
|
+
_pydevd=debugger,
|
25
|
+
_coverage=coverage,
|
26
|
+
)
|
27
|
+
|
28
|
+
self._debugger = debugger
|
29
|
+
self._coverage = coverage
|
30
|
+
self._extension_names = tuple(name for name, option in options.items() if option)
|
31
|
+
self._module_names = tuple(f'{_extensions.__name__}.{name}' for name in self._extension_names)
|
32
|
+
|
33
|
+
self.source_mapping: dict[str, str] = {}
|
34
|
+
|
35
|
+
@property
|
36
|
+
def debugger_enabled(self) -> bool:
|
37
|
+
"""Returns True if the debugger extension is enabled, otherwise False."""
|
38
|
+
return bool(self._debugger)
|
39
|
+
|
40
|
+
@property
|
41
|
+
def extension_names(self) -> tuple[str, ...]:
|
42
|
+
"""Names of extensions to include in the AnsiballZ payload."""
|
43
|
+
return self._extension_names
|
44
|
+
|
45
|
+
@property
|
46
|
+
def module_names(self) -> tuple[str, ...]:
|
47
|
+
"""Python module names of extensions to include in the AnsiballZ payload."""
|
48
|
+
return self._module_names
|
49
|
+
|
50
|
+
def get_extensions(self) -> dict[str, dict[str, object]]:
|
51
|
+
"""Return the configured extensions and their options."""
|
52
|
+
extension_options: dict[str, t.Any] = {}
|
53
|
+
|
54
|
+
if self._debugger:
|
55
|
+
extension_options['_pydevd'] = dataclasses.replace(
|
56
|
+
self._debugger,
|
57
|
+
source_mapping=self._get_source_mapping(),
|
58
|
+
)
|
59
|
+
|
60
|
+
if self._coverage:
|
61
|
+
extension_options['_coverage'] = self._coverage
|
62
|
+
|
63
|
+
extensions = {extension: dataclasses.asdict(options) for extension, options in extension_options.items()}
|
64
|
+
|
65
|
+
return extensions
|
66
|
+
|
67
|
+
def _get_source_mapping(self) -> dict[str, str]:
|
68
|
+
"""Get the source mapping, adjusting the source root as needed."""
|
69
|
+
if self._debugger.source_mapping:
|
70
|
+
source_mapping = {self._translate_path(key): value for key, value in self.source_mapping.items()}
|
71
|
+
else:
|
72
|
+
source_mapping = self.source_mapping
|
73
|
+
|
74
|
+
return source_mapping
|
75
|
+
|
76
|
+
def _translate_path(self, path: str) -> str:
|
77
|
+
"""Translate a local path to a foreign path."""
|
78
|
+
for replace, match in self._debugger.source_mapping.items():
|
79
|
+
if path.startswith(match):
|
80
|
+
return replace + path[len(match) :]
|
81
|
+
|
82
|
+
return path
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def create(cls, task_vars: dict[str, object]) -> t.Self:
|
86
|
+
"""Create an instance using the provided task vars."""
|
87
|
+
return cls(
|
88
|
+
debugger=cls._get_options('_ANSIBALLZ_DEBUGGER_CONFIG', _pydevd.Options, task_vars),
|
89
|
+
coverage=cls._get_options('_ANSIBALLZ_COVERAGE_CONFIG', _coverage.Options, task_vars),
|
90
|
+
)
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def _get_options(cls, name: str, config_type: type[_T], task_vars: dict[str, object]) -> _T | None:
|
94
|
+
"""Parse configuration from the named environment variable as the specified type, or None if not configured."""
|
95
|
+
if (value := config.get_config_value(name, variables=task_vars)) is None:
|
96
|
+
return None
|
97
|
+
|
98
|
+
data = json.loads(value) if isinstance(value, str) else value
|
99
|
+
options = config_type(**data)
|
100
|
+
|
101
|
+
return options
|
@@ -37,14 +37,13 @@ _ANSIBALLZ_WRAPPER = True
|
|
37
37
|
|
38
38
|
|
39
39
|
def _ansiballz_main(
|
40
|
-
|
40
|
+
zip_data: str,
|
41
41
|
ansible_module: str,
|
42
42
|
module_fqn: str,
|
43
43
|
params: str,
|
44
44
|
profile: str,
|
45
45
|
date_time: datetime.datetime,
|
46
|
-
|
47
|
-
coverage_output: str | None,
|
46
|
+
extensions: dict[str, dict[str, object]],
|
48
47
|
rlimit_nofile: int,
|
49
48
|
) -> None:
|
50
49
|
import os
|
@@ -136,15 +135,14 @@ def _ansiballz_main(
|
|
136
135
|
# can monkeypatch the right basic
|
137
136
|
sys.path.insert(0, modlib_path)
|
138
137
|
|
139
|
-
from ansible.module_utils._internal._ansiballz import
|
138
|
+
from ansible.module_utils._internal._ansiballz import _loader
|
140
139
|
|
141
|
-
run_module(
|
140
|
+
_loader.run_module(
|
142
141
|
json_params=json_params,
|
143
142
|
profile=profile,
|
144
143
|
module_fqn=module_fqn,
|
145
144
|
modlib_path=modlib_path,
|
146
|
-
|
147
|
-
coverage_output=coverage_output,
|
145
|
+
extensions=extensions,
|
148
146
|
)
|
149
147
|
|
150
148
|
def debug(command: str, modlib_path: str, json_params: bytes) -> None:
|
@@ -223,13 +221,14 @@ def _ansiballz_main(
|
|
223
221
|
with open(args_path, 'rb') as reader:
|
224
222
|
json_params = reader.read()
|
225
223
|
|
226
|
-
from ansible.module_utils._internal._ansiballz import
|
224
|
+
from ansible.module_utils._internal._ansiballz import _loader
|
227
225
|
|
228
|
-
run_module(
|
226
|
+
_loader.run_module(
|
229
227
|
json_params=json_params,
|
230
228
|
profile=profile,
|
231
229
|
module_fqn=module_fqn,
|
232
230
|
modlib_path=modlib_path,
|
231
|
+
extensions=extensions,
|
233
232
|
)
|
234
233
|
|
235
234
|
else:
|
@@ -246,13 +245,14 @@ def _ansiballz_main(
|
|
246
245
|
# store this in remote_tmpdir (use system tempdir instead)
|
247
246
|
# Only need to use [ansible_module]_payload_ in the temp_path until we move to zipimport
|
248
247
|
# (this helps ansible-test produce coverage stats)
|
249
|
-
|
248
|
+
# IMPORTANT: The real path must be used here to ensure a remote debugger such as PyCharm (using pydevd) can resolve paths correctly.
|
249
|
+
temp_path = os.path.realpath(tempfile.mkdtemp(prefix='ansible_' + ansible_module + '_payload_'))
|
250
250
|
|
251
251
|
try:
|
252
252
|
zipped_mod = os.path.join(temp_path, 'ansible_' + ansible_module + '_payload.zip')
|
253
253
|
|
254
254
|
with open(zipped_mod, 'wb') as modlib:
|
255
|
-
modlib.write(base64.b64decode(
|
255
|
+
modlib.write(base64.b64decode(zip_data))
|
256
256
|
|
257
257
|
if len(sys.argv) == 2:
|
258
258
|
debug(sys.argv[1], zipped_mod, encoded_params)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
import signal
|
5
|
+
import types
|
6
|
+
import typing as _t
|
7
|
+
|
8
|
+
from ansible.module_utils import datatag
|
9
|
+
|
10
|
+
|
11
|
+
class AnsibleTimeoutError(BaseException):
|
12
|
+
"""A general purpose timeout."""
|
13
|
+
|
14
|
+
_MAX_TIMEOUT = 100_000_000
|
15
|
+
"""
|
16
|
+
The maximum supported timeout value.
|
17
|
+
This value comes from BSD's alarm limit, which is due to that function using setitimer.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, timeout: int) -> None:
|
21
|
+
self.timeout = timeout
|
22
|
+
|
23
|
+
super().__init__(f"Timed out after {timeout} second(s).")
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
@contextlib.contextmanager
|
27
|
+
def alarm_timeout(cls, timeout: int | None) -> _t.Iterator[None]:
|
28
|
+
"""
|
29
|
+
Context for running code under an optional timeout.
|
30
|
+
Raises an instance of this class if the timeout occurs.
|
31
|
+
|
32
|
+
New usages of this timeout mechanism are discouraged.
|
33
|
+
"""
|
34
|
+
if timeout is not None:
|
35
|
+
if not isinstance(timeout, int):
|
36
|
+
raise TypeError(f"Timeout requires 'int' argument, not {datatag.native_type_name(timeout)!r}.")
|
37
|
+
|
38
|
+
if timeout < 0 or timeout > cls._MAX_TIMEOUT:
|
39
|
+
# On BSD based systems, alarm is implemented using setitimer.
|
40
|
+
# If out-of-bounds values are passed to alarm, they will return -1, which would be interpreted as an existing timer being set.
|
41
|
+
# To avoid that, bounds checking is performed in advance.
|
42
|
+
raise ValueError(f'Timeout {timeout} is invalid, it must be between 0 and {cls._MAX_TIMEOUT}.')
|
43
|
+
|
44
|
+
if not timeout:
|
45
|
+
yield # execute the context manager's body
|
46
|
+
return # no timeout to deal with, exit immediately
|
47
|
+
|
48
|
+
def on_alarm(_signal: int, _frame: types.FrameType) -> None:
|
49
|
+
raise cls(timeout)
|
50
|
+
|
51
|
+
if signal.signal(signal.SIGALRM, on_alarm):
|
52
|
+
raise RuntimeError("An existing alarm handler was present.")
|
53
|
+
|
54
|
+
try:
|
55
|
+
try:
|
56
|
+
if signal.alarm(timeout):
|
57
|
+
raise RuntimeError("An existing alarm was set.")
|
58
|
+
|
59
|
+
yield # execute the context manager's body
|
60
|
+
finally:
|
61
|
+
# Disable the alarm.
|
62
|
+
# If the alarm fires inside this finally block, the alarm is still disabled.
|
63
|
+
# This guarantees the cleanup code in the outer finally block runs without risk of encountering the `TaskTimeoutError` from the alarm.
|
64
|
+
signal.alarm(0)
|
65
|
+
finally:
|
66
|
+
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
@@ -1,10 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import collections.abc as _c
|
3
4
|
import dataclasses
|
4
5
|
import typing as t
|
5
6
|
|
7
|
+
from ansible._internal._errors import _error_utils
|
6
8
|
from ansible.errors import AnsibleRuntimeError
|
7
|
-
from ansible.module_utils.
|
9
|
+
from ansible.module_utils._internal import _messages
|
8
10
|
|
9
11
|
|
10
12
|
class AnsibleCapturedError(AnsibleRuntimeError):
|
@@ -16,43 +18,36 @@ class AnsibleCapturedError(AnsibleRuntimeError):
|
|
16
18
|
self,
|
17
19
|
*,
|
18
20
|
obj: t.Any = None,
|
19
|
-
|
21
|
+
event: _messages.Event,
|
20
22
|
) -> None:
|
21
23
|
super().__init__(
|
22
24
|
obj=obj,
|
23
25
|
)
|
24
26
|
|
25
|
-
self.
|
27
|
+
self._event = event
|
26
28
|
|
27
|
-
@property
|
28
|
-
def error_summary(self) -> ErrorSummary:
|
29
|
-
return self._error_summary
|
30
29
|
|
30
|
+
class AnsibleResultCapturedError(AnsibleCapturedError, _error_utils.ContributesToTaskResult):
|
31
|
+
"""
|
32
|
+
An exception representing error detail captured in a foreign context where an action/module result dictionary is involved.
|
31
33
|
|
32
|
-
|
33
|
-
"""
|
34
|
+
This exception provides a result dictionary via the ContributesToTaskResult mixin.
|
35
|
+
"""
|
34
36
|
|
35
|
-
def __init__(self,
|
36
|
-
super().__init__(
|
37
|
+
def __init__(self, event: _messages.Event, result: dict[str, t.Any]) -> None:
|
38
|
+
super().__init__(event=event)
|
37
39
|
|
38
40
|
self._result = result
|
39
41
|
|
42
|
+
@property
|
43
|
+
def result_contribution(self) -> _c.Mapping[str, object]:
|
44
|
+
return self._result
|
45
|
+
|
40
46
|
@classmethod
|
41
47
|
def maybe_raise_on_result(cls, result: dict[str, t.Any]) -> None:
|
42
48
|
"""Normalize the result and raise an exception if the result indicated failure."""
|
43
49
|
if error_summary := cls.normalize_result_exception(result):
|
44
|
-
raise error_summary.error_type(error_summary, result)
|
45
|
-
|
46
|
-
@classmethod
|
47
|
-
def find_first_remoted_error(cls, exception: BaseException) -> t.Self | None:
|
48
|
-
"""Find the first captured module error in the cause chain, starting with the given exception, returning None if not found."""
|
49
|
-
while exception:
|
50
|
-
if isinstance(exception, cls):
|
51
|
-
return exception
|
52
|
-
|
53
|
-
exception = exception.__cause__
|
54
|
-
|
55
|
-
return None
|
50
|
+
raise error_summary.error_type(error_summary.event, result)
|
56
51
|
|
57
52
|
@classmethod
|
58
53
|
def normalize_result_exception(cls, result: dict[str, t.Any]) -> CapturedErrorSummary | None:
|
@@ -76,17 +71,18 @@ class AnsibleResultCapturedError(AnsibleCapturedError):
|
|
76
71
|
|
77
72
|
if isinstance(exception, CapturedErrorSummary):
|
78
73
|
error_summary = exception
|
79
|
-
elif isinstance(exception, ErrorSummary):
|
74
|
+
elif isinstance(exception, _messages.ErrorSummary):
|
80
75
|
error_summary = CapturedErrorSummary(
|
81
|
-
|
82
|
-
formatted_traceback=cls._normalize_traceback(exception.formatted_traceback),
|
76
|
+
event=exception.event,
|
83
77
|
error_type=cls,
|
84
78
|
)
|
85
79
|
else:
|
86
80
|
# translate non-ErrorDetail errors
|
87
81
|
error_summary = CapturedErrorSummary(
|
88
|
-
|
89
|
-
|
82
|
+
event=_messages.Event(
|
83
|
+
msg=str(result.get('msg', 'Unknown error.')),
|
84
|
+
formatted_traceback=cls._normalize_traceback(exception),
|
85
|
+
),
|
90
86
|
error_type=cls,
|
91
87
|
)
|
92
88
|
|
@@ -122,7 +118,6 @@ class AnsibleModuleCapturedError(AnsibleResultCapturedError):
|
|
122
118
|
context = 'target'
|
123
119
|
|
124
120
|
|
125
|
-
@dataclasses.dataclass(**_dataclass_kwargs)
|
126
|
-
class CapturedErrorSummary(ErrorSummary):
|
127
|
-
# DTFIX-RELEASE: where to put this, name, etc. since it shows up in results, it's not exactly private (and contains a type ref to an internal type)
|
121
|
+
@dataclasses.dataclass(**_messages._dataclass_kwargs)
|
122
|
+
class CapturedErrorSummary(_messages.ErrorSummary):
|
128
123
|
error_type: type[AnsibleResultCapturedError] | None = None
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from __future__ import annotations as _annotations
|
2
|
+
|
3
|
+
from ansible.module_utils._internal import _errors, _messages
|
4
|
+
|
5
|
+
|
6
|
+
class ControllerEventFactory(_errors.EventFactory):
|
7
|
+
"""Factory for creating `Event` instances from `BaseException` instances on the controller."""
|
8
|
+
|
9
|
+
def _get_msg(self, exception: BaseException) -> str | None:
|
10
|
+
from ansible.errors import AnsibleError
|
11
|
+
|
12
|
+
if not isinstance(exception, AnsibleError):
|
13
|
+
return super()._get_msg(exception)
|
14
|
+
|
15
|
+
return exception._original_message.strip()
|
16
|
+
|
17
|
+
def _get_formatted_source_context(self, exception: BaseException) -> str | None:
|
18
|
+
from ansible.errors import AnsibleError
|
19
|
+
|
20
|
+
if not isinstance(exception, AnsibleError):
|
21
|
+
return super()._get_formatted_source_context(exception)
|
22
|
+
|
23
|
+
return exception._formatted_source_context
|
24
|
+
|
25
|
+
def _get_help_text(self, exception: BaseException) -> str | None:
|
26
|
+
from ansible.errors import AnsibleError
|
27
|
+
|
28
|
+
if not isinstance(exception, AnsibleError):
|
29
|
+
return super()._get_help_text(exception)
|
30
|
+
|
31
|
+
return exception._help_text
|
32
|
+
|
33
|
+
def _get_chain(self, exception: BaseException) -> _messages.EventChain | None:
|
34
|
+
from ansible._internal._errors import _captured # avoid circular import due to AnsibleError import
|
35
|
+
|
36
|
+
if isinstance(exception, _captured.AnsibleCapturedError):
|
37
|
+
# a captured error provides its own cause event, it never has a normal __cause__
|
38
|
+
return _messages.EventChain(
|
39
|
+
msg_reason=_errors.MSG_REASON_DIRECT_CAUSE,
|
40
|
+
traceback_reason=f'The above {exception.context} exception was the direct cause of the following controller exception:',
|
41
|
+
event=exception._event,
|
42
|
+
)
|
43
|
+
|
44
|
+
return super()._get_chain(exception)
|
45
|
+
|
46
|
+
def _follow_cause(self, exception: BaseException) -> bool:
|
47
|
+
from ansible.errors import AnsibleError
|
48
|
+
|
49
|
+
return not isinstance(exception, AnsibleError) or exception._include_cause_message
|
50
|
+
|
51
|
+
def _get_cause(self, exception: BaseException) -> BaseException | None:
|
52
|
+
# deprecated: description='remove support for orig_exc (deprecated in 2.23)' core_version='2.27'
|
53
|
+
|
54
|
+
cause = super()._get_cause(exception)
|
55
|
+
|
56
|
+
from ansible.errors import AnsibleError
|
57
|
+
|
58
|
+
if not isinstance(exception, AnsibleError):
|
59
|
+
return cause
|
60
|
+
|
61
|
+
try:
|
62
|
+
from ansible.utils.display import _display
|
63
|
+
except Exception: # pylint: disable=broad-except # if config is broken, this can raise things other than ImportError
|
64
|
+
_display = None
|
65
|
+
|
66
|
+
if cause:
|
67
|
+
if exception.orig_exc and exception.orig_exc is not cause and _display:
|
68
|
+
_display.warning(
|
69
|
+
msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given, but differed from the cause given by `raise ... from`.",
|
70
|
+
)
|
71
|
+
|
72
|
+
return cause
|
73
|
+
|
74
|
+
if exception.orig_exc:
|
75
|
+
if _display:
|
76
|
+
# encourage the use of `raise ... from` before deprecating `orig_exc`
|
77
|
+
_display.warning(
|
78
|
+
msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given without using `raise ... from orig_exc`.",
|
79
|
+
)
|
80
|
+
|
81
|
+
return exception.orig_exc
|
82
|
+
|
83
|
+
return None
|
84
|
+
|
85
|
+
def _get_events(self, exception: BaseException) -> tuple[_messages.Event, ...] | None:
|
86
|
+
if isinstance(exception, BaseExceptionGroup):
|
87
|
+
return tuple(self._convert_exception(ex) for ex in exception.exceptions)
|
88
|
+
|
89
|
+
return None
|