ansible-core 2.19.0b5__py3-none-any.whl → 2.19.0b7__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/_ansiballz/__init__.py +0 -0
- ansible/_internal/_ansiballz/_builder.py +101 -0
- ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
- ansible/_internal/_templating/_jinja_bits.py +22 -4
- ansible/_internal/_templating/_jinja_common.py +1 -1
- ansible/_internal/_templating/_jinja_plugins.py +5 -2
- ansible/_internal/_templating/_template_vars.py +72 -0
- ansible/_internal/_templating/_transform.py +6 -0
- ansible/_internal/_yaml/_constructor.py +4 -4
- ansible/_internal/_yaml/_dumper.py +26 -18
- ansible/cli/__init__.py +9 -14
- ansible/cli/adhoc.py +6 -3
- ansible/cli/arguments/option_helpers.py +1 -1
- ansible/cli/console.py +2 -2
- ansible/cli/doc.py +4 -4
- ansible/cli/inventory.py +5 -7
- ansible/config/base.yml +33 -6
- ansible/errors/__init__.py +2 -1
- ansible/executor/module_common.py +75 -44
- ansible/executor/powershell/psrp_put_file.ps1 +1 -1
- ansible/executor/process/worker.py +2 -2
- ansible/executor/task_executor.py +2 -2
- ansible/executor/task_queue_manager.py +34 -70
- ansible/executor/task_result.py +1 -1
- ansible/galaxy/api.py +3 -6
- ansible/galaxy/collection/__init__.py +1 -6
- ansible/galaxy/collection/concrete_artifact_manager.py +4 -10
- ansible/galaxy/dependency_resolution/providers.py +3 -3
- ansible/galaxy/role.py +2 -2
- ansible/inventory/group.py +6 -1
- ansible/inventory/host.py +6 -1
- ansible/module_utils/_internal/__init__.py +7 -4
- 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} +10 -38
- 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 +23 -1
- ansible/module_utils/_internal/_deprecator.py +39 -34
- ansible/module_utils/_internal/_json/_profiles/__init__.py +1 -0
- ansible/module_utils/_internal/_messages.py +26 -2
- ansible/module_utils/_internal/_plugin_info.py +14 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +58 -70
- ansible/module_utils/common/respawn.py +4 -41
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/connection.py +8 -11
- ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
- ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
- ansible/module_utils/facts/hardware/base.py +1 -1
- ansible/module_utils/facts/hardware/linux.py +1 -1
- ansible/module_utils/facts/other/facter.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 +2 -2
- ansible/module_utils/facts/system/local.py +1 -1
- ansible/module_utils/facts/virtual/linux.py +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
- ansible/module_utils/service.py +1 -1
- ansible/module_utils/urls.py +5 -5
- ansible/modules/apt.py +9 -3
- ansible/modules/apt_repository.py +10 -10
- ansible/modules/assemble.py +7 -5
- ansible/modules/async_wrapper.py +7 -17
- ansible/modules/command.py +3 -3
- ansible/modules/copy.py +4 -4
- ansible/modules/cron.py +1 -1
- ansible/modules/expect.py +5 -5
- 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 +2 -2
- ansible/modules/known_hosts.py +12 -14
- ansible/modules/package.py +6 -0
- ansible/modules/pip.py +9 -11
- ansible/modules/raw.py +2 -2
- ansible/modules/replace.py +2 -2
- ansible/modules/slurp.py +10 -13
- ansible/modules/stat.py +6 -8
- ansible/modules/unarchive.py +6 -6
- ansible/modules/user.py +1 -1
- ansible/modules/wait_for.py +38 -33
- ansible/modules/yum_repository.py +4 -3
- ansible/parsing/dataloader.py +2 -2
- ansible/parsing/mod_args.py +38 -20
- ansible/parsing/vault/__init__.py +9 -13
- ansible/playbook/base.py +7 -4
- ansible/playbook/helpers.py +1 -1
- ansible/playbook/included_file.py +3 -1
- ansible/playbook/play_context.py +2 -0
- ansible/playbook/playbook_include.py +23 -56
- ansible/playbook/role/__init__.py +38 -21
- ansible/playbook/taggable.py +19 -5
- ansible/playbook/task.py +2 -0
- ansible/plugins/action/__init__.py +2 -2
- ansible/plugins/action/assemble.py +2 -1
- ansible/plugins/action/assert.py +2 -2
- ansible/plugins/action/fetch.py +3 -3
- ansible/plugins/action/script.py +5 -4
- ansible/plugins/action/template.py +9 -3
- ansible/plugins/cache/__init__.py +17 -19
- ansible/plugins/callback/__init__.py +77 -87
- ansible/plugins/callback/default.py +0 -3
- ansible/plugins/callback/junit.py +0 -6
- ansible/plugins/callback/tree.py +5 -5
- ansible/plugins/connection/local.py +4 -4
- ansible/plugins/connection/paramiko_ssh.py +5 -5
- ansible/plugins/connection/ssh.py +9 -7
- ansible/plugins/connection/winrm.py +1 -1
- ansible/plugins/filter/core.py +19 -21
- ansible/plugins/filter/encryption.py +10 -2
- ansible/plugins/filter/pow.yml +1 -1
- ansible/plugins/filter/root.yml +1 -1
- ansible/plugins/filter/strftime.yml +3 -3
- ansible/plugins/filter/to_uuid.yml +1 -1
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +5 -4
- ansible/plugins/loader.py +5 -0
- ansible/plugins/lookup/password.py +4 -6
- ansible/plugins/lookup/template.py +9 -4
- ansible/plugins/shell/powershell.py +3 -2
- ansible/plugins/shell/sh.py +3 -2
- ansible/plugins/strategy/__init__.py +3 -3
- ansible/plugins/test/core.py +2 -2
- ansible/release.py +1 -1
- ansible/template/__init__.py +9 -53
- ansible/utils/collection_loader/_collection_finder.py +3 -3
- ansible/utils/display.py +38 -37
- ansible/utils/galaxy.py +2 -2
- ansible/utils/hashing.py +6 -7
- ansible/utils/path.py +6 -8
- ansible/utils/py3compat.py +2 -1
- ansible/utils/ssh_functions.py +3 -2
- ansible/utils/vars.py +4 -1
- ansible/vars/manager.py +6 -3
- ansible/vars/plugins.py +3 -3
- ansible/vars/reserved.py +6 -4
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +184 -173
- ansible_test/_internal/__init__.py +5 -0
- ansible_test/_internal/ansible_util.py +1 -1
- ansible_test/_internal/classification/python.py +6 -0
- ansible_test/_internal/cli/commands/__init__.py +0 -5
- ansible_test/_internal/cli/environments.py +51 -5
- ansible_test/_internal/commands/coverage/__init__.py +1 -1
- ansible_test/_internal/commands/integration/__init__.py +18 -5
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/integration/coverage.py +7 -2
- ansible_test/_internal/commands/sanity/__init__.py +3 -1
- ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
- ansible_test/_internal/commands/shell/__init__.py +43 -4
- ansible_test/_internal/commands/units/__init__.py +4 -1
- ansible_test/_internal/config.py +21 -13
- ansible_test/_internal/debugging.py +166 -0
- ansible_test/_internal/delegation.py +21 -13
- ansible_test/_internal/host_profiles.py +259 -16
- ansible_test/_internal/inventory.py +4 -0
- ansible_test/_internal/metadata.py +94 -4
- ansible_test/_internal/processes.py +80 -0
- ansible_test/_internal/provisioning.py +10 -4
- ansible_test/_internal/python_requirements.py +27 -0
- ansible_test/_internal/ssh.py +1 -5
- ansible_test/_internal/target.py +8 -0
- ansible_test/_internal/thread.py +2 -1
- ansible_test/_internal/timeout.py +1 -1
- ansible_test/_internal/util.py +20 -12
- ansible_test/_internal/util_common.py +13 -3
- ansible_test/_util/target/injector/python.py +8 -0
- ansible_test/_util/target/setup/requirements.py +3 -9
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,6 @@
|
|
5
5
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import errno
|
9
8
|
import fnmatch
|
10
9
|
import functools
|
11
10
|
import glob
|
@@ -1689,11 +1688,7 @@ def _extract_tar_dir(tar, dirname, b_dest):
|
|
1689
1688
|
b_dir_path = os.path.join(b_dest, to_bytes(dirname, errors='surrogate_or_strict'))
|
1690
1689
|
|
1691
1690
|
b_parent_path = os.path.dirname(b_dir_path)
|
1692
|
-
|
1693
|
-
os.makedirs(b_parent_path, mode=S_IRWXU_RXG_RXO)
|
1694
|
-
except OSError as e:
|
1695
|
-
if e.errno != errno.EEXIST:
|
1696
|
-
raise
|
1691
|
+
os.makedirs(b_parent_path, mode=S_IRWXU_RXG_RXO, exist_ok=True)
|
1697
1692
|
|
1698
1693
|
if tar_member.type == tarfile.SYMTYPE:
|
1699
1694
|
b_link_path = to_bytes(tar_member.linkname, errors='surrogate_or_strict')
|
@@ -449,9 +449,9 @@ def _extract_collection_from_git(repo_url, coll_ver, b_path):
|
|
449
449
|
except subprocess.CalledProcessError as proc_err:
|
450
450
|
raise AnsibleError( # should probably be LookupError
|
451
451
|
'Failed to switch a cloned Git repo `{repo_url!s}` '
|
452
|
-
'to the requested revision `{
|
452
|
+
'to the requested revision `{revision!s}`.'.
|
453
453
|
format(
|
454
|
-
|
454
|
+
revision=to_native(version),
|
455
455
|
repo_url=to_native(git_url),
|
456
456
|
),
|
457
457
|
) from proc_err
|
@@ -656,14 +656,8 @@ def _get_json_from_installed_dir(
|
|
656
656
|
try:
|
657
657
|
with open(b_json_filepath, 'rb') as manifest_fd:
|
658
658
|
b_json_text = manifest_fd.read()
|
659
|
-
except
|
660
|
-
raise LookupError(
|
661
|
-
"The collection {manifest!s} path '{path!s}' does not exist.".
|
662
|
-
format(
|
663
|
-
manifest=filename,
|
664
|
-
path=to_native(b_json_filepath),
|
665
|
-
)
|
666
|
-
)
|
659
|
+
except OSError as ex:
|
660
|
+
raise LookupError(f"The collection {filename!r} path {to_text(b_json_filepath)!r} does not exist.") from ex
|
667
661
|
|
668
662
|
manifest_txt = to_text(b_json_text, errors='surrogate_or_strict')
|
669
663
|
|
@@ -148,7 +148,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
|
|
148
148
|
|
149
149
|
:param resolutions: Mapping of identifier, candidate pairs.
|
150
150
|
|
151
|
-
:param candidates: Possible candidates for the
|
151
|
+
:param candidates: Possible candidates for the identifier.
|
152
152
|
Mapping of identifier, list of candidate pairs.
|
153
153
|
|
154
154
|
:param information: Requirement information of each package.
|
@@ -158,7 +158,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
|
|
158
158
|
:param backtrack_causes: Sequence of requirement information that were
|
159
159
|
the requirements that caused the resolver to most recently backtrack.
|
160
160
|
|
161
|
-
The preference could depend on
|
161
|
+
The preference could depend on various of issues, including
|
162
162
|
(not necessarily in this order):
|
163
163
|
|
164
164
|
* Is this package pinned in the current resolution result?
|
@@ -404,7 +404,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
|
|
404
404
|
|
405
405
|
:param requirement: A requirement that produced the `candidate`.
|
406
406
|
|
407
|
-
:param candidate: A pinned candidate supposedly
|
407
|
+
:param candidate: A pinned candidate supposedly matching the \
|
408
408
|
`requirement` specifier. It is guaranteed to \
|
409
409
|
have been generated from the `requirement`.
|
410
410
|
|
ansible/galaxy/role.py
CHANGED
@@ -438,8 +438,8 @@ class GalaxyRole(object):
|
|
438
438
|
if not (self.src and os.path.isfile(self.src)):
|
439
439
|
try:
|
440
440
|
os.unlink(tmp_file)
|
441
|
-
except
|
442
|
-
display.
|
441
|
+
except OSError as ex:
|
442
|
+
display.error_as_warning(f"Unable to remove tmp file {tmp_file!r}.", exception=ex)
|
443
443
|
return True
|
444
444
|
|
445
445
|
return False
|
ansible/inventory/group.py
CHANGED
@@ -26,7 +26,7 @@ from ansible import constants as C
|
|
26
26
|
from ansible.errors import AnsibleError
|
27
27
|
from ansible.module_utils.common.text.converters import to_native, to_text
|
28
28
|
from ansible.utils.display import Display
|
29
|
-
from ansible.utils.vars import combine_vars
|
29
|
+
from ansible.utils.vars import combine_vars, validate_variable_name
|
30
30
|
|
31
31
|
from . import helpers # this is left as a module import to facilitate easier unit test patching
|
32
32
|
|
@@ -221,6 +221,11 @@ class Group:
|
|
221
221
|
def set_variable(self, key: str, value: t.Any) -> None:
|
222
222
|
key = helpers.remove_trust(key)
|
223
223
|
|
224
|
+
try:
|
225
|
+
validate_variable_name(key)
|
226
|
+
except AnsibleError as ex:
|
227
|
+
Display().deprecated(msg=f'Accepting inventory variable with invalid name {key!r}.', version='2.23', help_text=ex._help_text, obj=ex.obj)
|
228
|
+
|
224
229
|
if key == 'ansible_group_priority':
|
225
230
|
self.set_priority(int(value))
|
226
231
|
else:
|
ansible/inventory/host.py
CHANGED
@@ -22,8 +22,10 @@ import typing as t
|
|
22
22
|
|
23
23
|
from collections.abc import Mapping, MutableMapping
|
24
24
|
|
25
|
+
from ansible.errors import AnsibleError
|
25
26
|
from ansible.inventory.group import Group, InventoryObjectType
|
26
27
|
from ansible.parsing.utils.addresses import patterns
|
28
|
+
from ansible.utils.display import Display
|
27
29
|
from ansible.utils.vars import combine_vars, get_unique_id, validate_variable_name
|
28
30
|
|
29
31
|
from . import helpers # this is left as a module import to facilitate easier unit test patching
|
@@ -117,7 +119,10 @@ class Host:
|
|
117
119
|
def set_variable(self, key: str, value: t.Any) -> None:
|
118
120
|
key = helpers.remove_trust(key)
|
119
121
|
|
120
|
-
|
122
|
+
try:
|
123
|
+
validate_variable_name(key)
|
124
|
+
except AnsibleError as ex:
|
125
|
+
Display().deprecated(msg=f'Accepting inventory variable with invalid name {key!r}.', version='2.23', help_text=ex._help_text, obj=ex.obj)
|
121
126
|
|
122
127
|
if key in self.vars and isinstance(self.vars[key], MutableMapping) and isinstance(value, Mapping):
|
123
128
|
self.vars = combine_vars(self.vars, {key: value})
|
@@ -4,6 +4,9 @@ import collections.abc as c
|
|
4
4
|
|
5
5
|
import typing as t
|
6
6
|
|
7
|
+
if t.TYPE_CHECKING:
|
8
|
+
from ansible.module_utils.compat.typing import TypeGuard
|
9
|
+
|
7
10
|
|
8
11
|
INTERMEDIATE_MAPPING_TYPES = (c.Mapping,)
|
9
12
|
"""
|
@@ -18,18 +21,18 @@ These will be converted to a simple Python `list` before serialization or storag
|
|
18
21
|
CAUTION: Scalar types which are sequences should be excluded when using this.
|
19
22
|
"""
|
20
23
|
|
21
|
-
|
24
|
+
ITERABLE_SCALARS_NOT_TO_ITERATE = (str, bytes)
|
22
25
|
"""Scalars which are also iterable, and should thus be excluded from iterable checks."""
|
23
26
|
|
24
27
|
|
25
|
-
def is_intermediate_mapping(value: object) ->
|
28
|
+
def is_intermediate_mapping(value: object) -> TypeGuard[c.Mapping]:
|
26
29
|
"""Returns `True` if `value` is a type supported for projection to a Python `dict`, otherwise returns `False`."""
|
27
30
|
return isinstance(value, INTERMEDIATE_MAPPING_TYPES)
|
28
31
|
|
29
32
|
|
30
|
-
def is_intermediate_iterable(value: object) ->
|
33
|
+
def is_intermediate_iterable(value: object) -> TypeGuard[c.Iterable]:
|
31
34
|
"""Returns `True` if `value` is a type supported for projection to a Python `list`, otherwise returns `False`."""
|
32
|
-
return isinstance(value, INTERMEDIATE_ITERABLE_TYPES) and not isinstance(value,
|
35
|
+
return isinstance(value, INTERMEDIATE_ITERABLE_TYPES) and not isinstance(value, ITERABLE_SCALARS_NOT_TO_ITERATE)
|
33
36
|
|
34
37
|
|
35
38
|
is_controller: bool = False
|
File without changes
|
File without changes
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import atexit
|
4
|
+
import dataclasses
|
5
|
+
import importlib.util
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
|
9
|
+
import typing as t
|
10
|
+
|
11
|
+
|
12
|
+
@dataclasses.dataclass(frozen=True)
|
13
|
+
class Options:
|
14
|
+
"""Code coverage options."""
|
15
|
+
|
16
|
+
config: str
|
17
|
+
output: str | None
|
18
|
+
|
19
|
+
|
20
|
+
def run(args: dict[str, t.Any]) -> None: # pragma: nocover
|
21
|
+
"""Bootstrap `coverage` for the current Ansible module invocation."""
|
22
|
+
options = Options(**args)
|
23
|
+
|
24
|
+
if options.output:
|
25
|
+
# Enable code coverage analysis of the module.
|
26
|
+
# This feature is for internal testing and may change without notice.
|
27
|
+
python_version_string = '.'.join(str(v) for v in sys.version_info[:2])
|
28
|
+
os.environ['COVERAGE_FILE'] = f'{options.output}=python-{python_version_string}=coverage'
|
29
|
+
|
30
|
+
import coverage
|
31
|
+
|
32
|
+
cov = coverage.Coverage(config_file=options.config)
|
33
|
+
|
34
|
+
def atexit_coverage() -> None:
|
35
|
+
cov.stop()
|
36
|
+
cov.save()
|
37
|
+
|
38
|
+
atexit.register(atexit_coverage)
|
39
|
+
|
40
|
+
cov.start()
|
41
|
+
else:
|
42
|
+
# Verify coverage is available without importing it.
|
43
|
+
# This will detect when a module would fail with coverage enabled with minimal overhead.
|
44
|
+
if importlib.util.find_spec('coverage') is None:
|
45
|
+
raise RuntimeError('Could not find the `coverage` Python module.')
|
@@ -0,0 +1,62 @@
|
|
1
|
+
"""
|
2
|
+
Remote debugging support for AnsiballZ modules.
|
3
|
+
|
4
|
+
To use with PyCharm:
|
5
|
+
|
6
|
+
1) Choose an available port for PyCharm to listen on (e.g. 5678).
|
7
|
+
2) Create a Python Debug Server using that port.
|
8
|
+
3) Start the Python Debug Server.
|
9
|
+
4) Ensure the correct version of `pydevd-pycharm` is installed for the interpreter(s) which will run the code being debugged.
|
10
|
+
5) Configure Ansible with the `_ANSIBALLZ_DEBUGGER_CONFIG` option.
|
11
|
+
See `Options` below for the structure of the debugger configuration.
|
12
|
+
Example configuration using an environment variable:
|
13
|
+
export _ANSIBLE_ANSIBALLZ_DEBUGGER_CONFIG='{"module": "pydevd_pycharm", "settrace": {"host": "localhost", "port": 5678, "suspend": false}}'
|
14
|
+
6) Set any desired breakpoints.
|
15
|
+
7) Run Ansible commands.
|
16
|
+
|
17
|
+
A similar process should work for other pydevd based debuggers, such as Visual Studio Code, but they have not been tested.
|
18
|
+
"""
|
19
|
+
|
20
|
+
from __future__ import annotations
|
21
|
+
|
22
|
+
import dataclasses
|
23
|
+
import importlib
|
24
|
+
import json
|
25
|
+
import os
|
26
|
+
import pathlib
|
27
|
+
|
28
|
+
import typing as t
|
29
|
+
|
30
|
+
|
31
|
+
@dataclasses.dataclass(frozen=True)
|
32
|
+
class Options:
|
33
|
+
"""Debugger options for pydevd and its derivatives."""
|
34
|
+
|
35
|
+
module: str = 'pydevd'
|
36
|
+
"""The Python module which will be imported and which provides the `settrace` method."""
|
37
|
+
settrace: dict[str, object] = dataclasses.field(default_factory=dict)
|
38
|
+
"""The options to pass to the `{module}.settrace` method."""
|
39
|
+
source_mapping: dict[str, str] = dataclasses.field(default_factory=dict)
|
40
|
+
"""
|
41
|
+
A mapping of source paths to provide to pydevd.
|
42
|
+
This setting is used internally by AnsiballZ and is not required unless Ansible CLI commands are run from a different system than your IDE.
|
43
|
+
In that scenario, use this setting instead of configuring source mapping in your IDE.
|
44
|
+
The key is a path known to the IDE.
|
45
|
+
The value is the same path as known to the Ansible CLI.
|
46
|
+
Both file paths and directories are supported.
|
47
|
+
"""
|
48
|
+
|
49
|
+
|
50
|
+
def run(args: dict[str, t.Any]) -> None: # pragma: nocover
|
51
|
+
"""Enable remote debugging."""
|
52
|
+
|
53
|
+
options = Options(**args)
|
54
|
+
temp_dir = pathlib.Path(__file__).parent.parent.parent.parent.parent.parent
|
55
|
+
path_mapping = [[key, str(temp_dir / value)] for key, value in options.source_mapping.items()]
|
56
|
+
|
57
|
+
os.environ['PATHS_FROM_ECLIPSE_TO_PYTHON'] = json.dumps(path_mapping)
|
58
|
+
|
59
|
+
debugging_module = importlib.import_module(options.module)
|
60
|
+
debugging_module.settrace(**options.settrace)
|
61
|
+
|
62
|
+
pass # when suspend is True, execution pauses here -- it's also a convenient place to put a breakpoint
|
@@ -5,17 +5,15 @@
|
|
5
5
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import
|
9
|
-
import importlib.util
|
8
|
+
import importlib
|
10
9
|
import json
|
11
|
-
import os
|
12
10
|
import runpy
|
13
11
|
import sys
|
14
12
|
import typing as t
|
15
13
|
|
16
|
-
from . import
|
17
|
-
from
|
18
|
-
from
|
14
|
+
from ansible.module_utils import basic
|
15
|
+
from ansible.module_utils._internal import _errors, _traceback, _messages, _ansiballz
|
16
|
+
from ansible.module_utils.common.json import get_module_encoder, Direction
|
19
17
|
|
20
18
|
|
21
19
|
def run_module(
|
@@ -24,13 +22,16 @@ def run_module(
|
|
24
22
|
profile: str,
|
25
23
|
module_fqn: str,
|
26
24
|
modlib_path: str,
|
25
|
+
extensions: dict[str, dict[str, object]],
|
27
26
|
init_globals: dict[str, t.Any] | None = None,
|
28
|
-
coverage_config: str | None = None,
|
29
|
-
coverage_output: str | None = None,
|
30
27
|
) -> None: # pragma: nocover
|
31
28
|
"""Used internally by the AnsiballZ wrapper to run an Ansible module."""
|
32
29
|
try:
|
33
|
-
|
30
|
+
for extension, args in extensions.items():
|
31
|
+
# importing _ansiballz instead of _extensions avoids an unnecessary import when extensions are not in use
|
32
|
+
extension_module = importlib.import_module(f'{_ansiballz.__name__}._extensions.{extension}')
|
33
|
+
extension_module.run(args)
|
34
|
+
|
34
35
|
_run_module(
|
35
36
|
json_params=json_params,
|
36
37
|
profile=profile,
|
@@ -42,35 +43,6 @@ def run_module(
|
|
42
43
|
_handle_exception(ex, profile)
|
43
44
|
|
44
45
|
|
45
|
-
def _enable_coverage(coverage_config: str | None, coverage_output: str | None) -> None: # pragma: nocover
|
46
|
-
"""Bootstrap `coverage` for the current Ansible module invocation."""
|
47
|
-
if not coverage_config:
|
48
|
-
return
|
49
|
-
|
50
|
-
if coverage_output:
|
51
|
-
# Enable code coverage analysis of the module.
|
52
|
-
# This feature is for internal testing and may change without notice.
|
53
|
-
python_version_string = '.'.join(str(v) for v in sys.version_info[:2])
|
54
|
-
os.environ['COVERAGE_FILE'] = f'{coverage_output}=python-{python_version_string}=coverage'
|
55
|
-
|
56
|
-
import coverage
|
57
|
-
|
58
|
-
cov = coverage.Coverage(config_file=coverage_config)
|
59
|
-
|
60
|
-
def atexit_coverage():
|
61
|
-
cov.stop()
|
62
|
-
cov.save()
|
63
|
-
|
64
|
-
atexit.register(atexit_coverage)
|
65
|
-
|
66
|
-
cov.start()
|
67
|
-
else:
|
68
|
-
# Verify coverage is available without importing it.
|
69
|
-
# This will detect when a module would fail with coverage enabled with minimal overhead.
|
70
|
-
if importlib.util.find_spec('coverage') is None:
|
71
|
-
raise RuntimeError('Could not find the `coverage` Python module.')
|
72
|
-
|
73
|
-
|
74
46
|
def _run_module(
|
75
47
|
*,
|
76
48
|
json_params: bytes,
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
import sys
|
5
|
+
|
6
|
+
from ... import basic
|
7
|
+
from . import _respawn_wrapper
|
8
|
+
|
9
|
+
|
10
|
+
def create_payload() -> str:
|
11
|
+
"""Create and return an AnsiballZ payload for respawning a module."""
|
12
|
+
main = sys.modules['__main__']
|
13
|
+
code = inspect.getsource(_respawn_wrapper)
|
14
|
+
|
15
|
+
args = dict(
|
16
|
+
module_fqn=main._module_fqn,
|
17
|
+
modlib_path=main._modlib_path,
|
18
|
+
profile=basic._ANSIBLE_PROFILE,
|
19
|
+
json_params=basic._ANSIBLE_ARGS,
|
20
|
+
)
|
21
|
+
|
22
|
+
args_string = '\n'.join(f'{key}={value!r},' for key, value in args.items())
|
23
|
+
|
24
|
+
wrapper = f"""{code}
|
25
|
+
|
26
|
+
if __name__ == "__main__":
|
27
|
+
_respawn_main(
|
28
|
+
{args_string}
|
29
|
+
)
|
30
|
+
"""
|
31
|
+
|
32
|
+
return wrapper
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
|
4
|
+
def _respawn_main(
|
5
|
+
json_params: bytes,
|
6
|
+
profile: str,
|
7
|
+
module_fqn: str,
|
8
|
+
modlib_path: str,
|
9
|
+
) -> None:
|
10
|
+
import sys
|
11
|
+
|
12
|
+
sys.path.insert(0, modlib_path)
|
13
|
+
|
14
|
+
from ansible.module_utils._internal._ansiballz import _loader
|
15
|
+
|
16
|
+
_loader.run_module(
|
17
|
+
json_params=json_params,
|
18
|
+
profile=profile,
|
19
|
+
module_fqn=module_fqn,
|
20
|
+
modlib_path=modlib_path,
|
21
|
+
extensions={},
|
22
|
+
init_globals=dict(_respawned=True),
|
23
|
+
)
|
@@ -5,6 +5,7 @@ import collections.abc as c
|
|
5
5
|
import copy
|
6
6
|
import dataclasses
|
7
7
|
import datetime
|
8
|
+
import enum
|
8
9
|
import inspect
|
9
10
|
import sys
|
10
11
|
|
@@ -216,7 +217,7 @@ class AnsibleTagHelper:
|
|
216
217
|
return value
|
217
218
|
|
218
219
|
|
219
|
-
class AnsibleSerializable
|
220
|
+
class AnsibleSerializable:
|
220
221
|
__slots__ = _NO_INSTANCE_STORAGE
|
221
222
|
|
222
223
|
_known_type_map: t.ClassVar[t.Dict[str, t.Type['AnsibleSerializable']]] = {}
|
@@ -274,6 +275,27 @@ class AnsibleSerializable(metaclass=abc.ABCMeta):
|
|
274
275
|
return f'{name}({arg_string})'
|
275
276
|
|
276
277
|
|
278
|
+
class AnsibleSerializableEnum(AnsibleSerializable, enum.Enum):
|
279
|
+
"""Base class for serializable enumerations."""
|
280
|
+
|
281
|
+
def _as_dict(self) -> t.Dict[str, t.Any]:
|
282
|
+
return dict(value=self.value)
|
283
|
+
|
284
|
+
@classmethod
|
285
|
+
def _from_dict(cls, d: t.Dict[str, t.Any]) -> t.Self:
|
286
|
+
return cls(d['value'].lower())
|
287
|
+
|
288
|
+
def __str__(self) -> str:
|
289
|
+
return self.value
|
290
|
+
|
291
|
+
def __repr__(self) -> str:
|
292
|
+
return f'<{self.__class__.__name__}.{self.name}>'
|
293
|
+
|
294
|
+
@staticmethod
|
295
|
+
def _generate_next_value_(name, start, count, last_values):
|
296
|
+
return name.lower()
|
297
|
+
|
298
|
+
|
277
299
|
class AnsibleSerializableWrapper(AnsibleSerializable, t.Generic[_T], metaclass=abc.ABCMeta):
|
278
300
|
__slots__ = ('_value',)
|
279
301
|
|
@@ -5,7 +5,7 @@ import pathlib
|
|
5
5
|
import sys
|
6
6
|
import typing as t
|
7
7
|
|
8
|
-
from ansible.module_utils._internal import _stack, _messages, _validation
|
8
|
+
from ansible.module_utils._internal import _stack, _messages, _validation, _plugin_info
|
9
9
|
|
10
10
|
|
11
11
|
def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None:
|
@@ -19,7 +19,7 @@ def deprecator_from_collection_name(collection_name: str | None) -> _messages.Pl
|
|
19
19
|
|
20
20
|
return _messages.PluginInfo(
|
21
21
|
resolved_name=collection_name,
|
22
|
-
type=
|
22
|
+
type=None,
|
23
23
|
)
|
24
24
|
|
25
25
|
|
@@ -38,11 +38,16 @@ def get_caller_plugin_info() -> _messages.PluginInfo | None:
|
|
38
38
|
_skip_stackwalk = True
|
39
39
|
|
40
40
|
if frame_info := _stack.caller_frame():
|
41
|
-
return
|
41
|
+
return _path_as_plugininfo(frame_info.filename)
|
42
42
|
|
43
43
|
return None # pragma: nocover
|
44
44
|
|
45
45
|
|
46
|
+
def _path_as_plugininfo(path: str) -> _messages.PluginInfo | None:
|
47
|
+
"""Return a `PluginInfo` instance if the provided `path` refers to a plugin."""
|
48
|
+
return _path_as_core_plugininfo(path) or _path_as_collection_plugininfo(path)
|
49
|
+
|
50
|
+
|
46
51
|
def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
|
47
52
|
"""Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
|
48
53
|
try:
|
@@ -54,7 +59,7 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
|
|
54
59
|
|
55
60
|
if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
|
56
61
|
plugin_name = match.group("plugin_name")
|
57
|
-
plugin_type = match.group("plugin_type")
|
62
|
+
plugin_type = _plugin_info.normalize_plugin_type(match.group("plugin_type"))
|
58
63
|
|
59
64
|
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
60
65
|
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
@@ -62,16 +67,20 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
|
|
62
67
|
# Callers in this case need to identify the deprecating plugin name, otherwise only ansible-core will be reported.
|
63
68
|
# Reporting ansible-core is never wrong, it just may be missing an additional detail (plugin name) in the "on behalf of" case.
|
64
69
|
return ANSIBLE_CORE_DEPRECATOR
|
70
|
+
|
71
|
+
if plugin_name == '__init__':
|
72
|
+
# The plugin type is known, but the caller isn't a specific plugin -- instead, it's core plugin infrastructure (the base class).
|
73
|
+
return _messages.PluginInfo(resolved_name=namespace, type=plugin_type)
|
65
74
|
elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
|
66
75
|
# AnsiballZ Python package for core modules
|
67
76
|
plugin_name = match.group("module_name")
|
68
|
-
plugin_type =
|
77
|
+
plugin_type = _messages.PluginType.MODULE
|
69
78
|
elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
|
70
79
|
# AnsiballZ Python package for non-core library/role modules
|
71
80
|
namespace = 'ansible.legacy'
|
72
81
|
|
73
82
|
plugin_name = match.group("module_name")
|
74
|
-
plugin_type =
|
83
|
+
plugin_type = _messages.PluginType.MODULE
|
75
84
|
else:
|
76
85
|
return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above
|
77
86
|
|
@@ -85,7 +94,7 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
|
|
85
94
|
if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
|
86
95
|
return None
|
87
96
|
|
88
|
-
plugin_type = match.group('plugin_type')
|
97
|
+
plugin_type = _plugin_info.normalize_plugin_type(match.group('plugin_type'))
|
89
98
|
|
90
99
|
if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
|
91
100
|
# We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
|
@@ -93,9 +102,6 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
|
|
93
102
|
# In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
|
94
103
|
return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
|
95
104
|
|
96
|
-
if plugin_type == 'modules':
|
97
|
-
plugin_type = 'module'
|
98
|
-
|
99
105
|
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
100
106
|
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
101
107
|
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
|
@@ -104,11 +110,10 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
|
|
104
110
|
|
105
111
|
name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
|
106
112
|
|
107
|
-
|
113
|
+
# DTFIX-FUTURE: deprecations from __init__ will be incorrectly attributed to a plugin of that name
|
108
114
|
|
115
|
+
return _messages.PluginInfo(resolved_name=name, type=plugin_type)
|
109
116
|
|
110
|
-
_COLLECTION_ONLY_TYPE: t.Final = 'collection'
|
111
|
-
"""Ersatz placeholder plugin type for use by a `PluginInfo` instance that references only a collection."""
|
112
117
|
|
113
118
|
_ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
114
119
|
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
@@ -116,37 +121,37 @@ _ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file_
|
|
116
121
|
ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
|
117
122
|
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
|
118
123
|
|
119
|
-
INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name=
|
124
|
+
INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name=None, type=None)
|
120
125
|
"""Singleton `PluginInfo` instance for indeterminate deprecator."""
|
121
126
|
|
122
127
|
_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
123
128
|
{
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
#
|
131
|
-
#
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
#
|
141
|
-
|
129
|
+
_messages.PluginType.ACTION,
|
130
|
+
_messages.PluginType.BECOME,
|
131
|
+
_messages.PluginType.CACHE,
|
132
|
+
_messages.PluginType.CALLBACK,
|
133
|
+
_messages.PluginType.CLICONF,
|
134
|
+
_messages.PluginType.CONNECTION,
|
135
|
+
# DOC_FRAGMENTS - no code execution
|
136
|
+
# FILTER - basename inadequate to identify plugin
|
137
|
+
_messages.PluginType.HTTPAPI,
|
138
|
+
_messages.PluginType.INVENTORY,
|
139
|
+
_messages.PluginType.LOOKUP,
|
140
|
+
_messages.PluginType.MODULE, # only for collections
|
141
|
+
_messages.PluginType.NETCONF,
|
142
|
+
_messages.PluginType.SHELL,
|
143
|
+
_messages.PluginType.STRATEGY,
|
144
|
+
_messages.PluginType.TERMINAL,
|
145
|
+
# TEST - basename inadequate to identify plugin
|
146
|
+
_messages.PluginType.VARS,
|
142
147
|
}
|
143
148
|
)
|
144
149
|
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
145
150
|
|
146
151
|
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
147
152
|
{
|
148
|
-
|
149
|
-
|
153
|
+
_messages.PluginType.FILTER,
|
154
|
+
_messages.PluginType.TEST,
|
150
155
|
}
|
151
156
|
)
|
152
157
|
"""Plugin types for which basename cannot be used to identify the plugin name."""
|
@@ -87,6 +87,7 @@ For controller-to-module, type behavior is profile dependent.
|
|
87
87
|
_common_module_response_types: frozenset[type[AnsibleSerializable]] = frozenset(
|
88
88
|
{
|
89
89
|
_messages.PluginInfo,
|
90
|
+
_messages.PluginType,
|
90
91
|
_messages.Event,
|
91
92
|
_messages.EventChain,
|
92
93
|
_messages.ErrorSummary,
|