ansible-core 2.19.0b2__py3-none-any.whl → 2.19.0b4__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.py +1 -4
- ansible/_internal/_json/__init__.py +1 -1
- ansible/_internal/_templating/_datatag.py +3 -4
- ansible/_internal/_templating/_engine.py +6 -1
- ansible/_internal/_templating/_jinja_plugins.py +2 -6
- ansible/_internal/_testing.py +26 -0
- ansible/cli/__init__.py +3 -2
- ansible/cli/arguments/option_helpers.py +10 -3
- ansible/cli/doc.py +0 -1
- ansible/config/base.yml +5 -23
- ansible/config/manager.py +144 -103
- ansible/constants.py +1 -63
- ansible/errors/__init__.py +6 -2
- ansible/executor/module_common.py +11 -7
- ansible/executor/task_executor.py +6 -8
- ansible/galaxy/api.py +1 -1
- ansible/galaxy/collection/__init__.py +3 -3
- ansible/inventory/manager.py +1 -0
- ansible/module_utils/_internal/_ansiballz.py +4 -30
- ansible/module_utils/_internal/_datatag/_tags.py +3 -25
- ansible/module_utils/_internal/_deprecator.py +134 -0
- ansible/module_utils/_internal/_plugin_info.py +25 -0
- ansible/module_utils/_internal/_validation.py +14 -0
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +64 -17
- ansible/module_utils/common/arg_spec.py +8 -3
- ansible/module_utils/common/messages.py +40 -23
- ansible/module_utils/common/process.py +0 -1
- ansible/module_utils/common/respawn.py +0 -7
- ansible/module_utils/common/warnings.py +13 -13
- ansible/module_utils/datatag.py +13 -13
- ansible/module_utils/facts/virtual/linux.py +1 -1
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +1 -1
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +2 -1
- ansible/modules/get_url.py +1 -1
- ansible/modules/git.py +1 -6
- ansible/modules/pip.py +2 -4
- ansible/modules/sysvinit.py +3 -3
- ansible/playbook/task.py +0 -2
- ansible/plugins/__init__.py +18 -8
- ansible/plugins/action/__init__.py +7 -15
- ansible/plugins/action/gather_facts.py +2 -4
- ansible/plugins/action/template.py +3 -0
- ansible/plugins/callback/oneline.py +7 -1
- ansible/plugins/callback/tree.py +7 -1
- ansible/plugins/connection/local.py +1 -1
- ansible/plugins/connection/paramiko_ssh.py +9 -2
- ansible/plugins/doc_fragments/action_core.py +1 -1
- ansible/plugins/filter/core.py +4 -1
- ansible/plugins/inventory/__init__.py +2 -2
- ansible/plugins/loader.py +197 -132
- ansible/plugins/lookup/url.py +2 -2
- ansible/plugins/strategy/__init__.py +6 -6
- ansible/release.py +1 -1
- ansible/template/__init__.py +1 -1
- ansible/utils/collection_loader/__init__.py +2 -0
- ansible/utils/collection_loader/_collection_meta.py +5 -3
- ansible/utils/display.py +137 -71
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/py3compat.py +1 -7
- ansible/utils/ssh_functions.py +4 -1
- ansible/vars/manager.py +18 -10
- ansible/vars/plugins.py +4 -4
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/RECORD +82 -79
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/WHEEL +1 -1
- ansible_test/_internal/commands/sanity/pylint.py +1 -0
- ansible_test/_internal/docker_util.py +4 -3
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +486 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
- ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
- ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import atexit
|
9
|
-
import dataclasses
|
10
9
|
import importlib.util
|
11
10
|
import json
|
12
11
|
import os
|
@@ -15,17 +14,14 @@ import sys
|
|
15
14
|
import typing as t
|
16
15
|
|
17
16
|
from . import _errors
|
18
|
-
from ._plugin_exec_context import PluginExecContext, HasPluginInfo
|
19
17
|
from .. import basic
|
20
18
|
from ..common.json import get_module_encoder, Direction
|
21
|
-
from ..common.messages import PluginInfo
|
22
19
|
|
23
20
|
|
24
21
|
def run_module(
|
25
22
|
*,
|
26
23
|
json_params: bytes,
|
27
24
|
profile: str,
|
28
|
-
plugin_info_dict: dict[str, object],
|
29
25
|
module_fqn: str,
|
30
26
|
modlib_path: str,
|
31
27
|
init_globals: dict[str, t.Any] | None = None,
|
@@ -38,7 +34,6 @@ def run_module(
|
|
38
34
|
_run_module(
|
39
35
|
json_params=json_params,
|
40
36
|
profile=profile,
|
41
|
-
plugin_info_dict=plugin_info_dict,
|
42
37
|
module_fqn=module_fqn,
|
43
38
|
modlib_path=modlib_path,
|
44
39
|
init_globals=init_globals,
|
@@ -80,7 +75,6 @@ def _run_module(
|
|
80
75
|
*,
|
81
76
|
json_params: bytes,
|
82
77
|
profile: str,
|
83
|
-
plugin_info_dict: dict[str, object],
|
84
78
|
module_fqn: str,
|
85
79
|
modlib_path: str,
|
86
80
|
init_globals: dict[str, t.Any] | None = None,
|
@@ -92,12 +86,11 @@ def _run_module(
|
|
92
86
|
init_globals = init_globals or {}
|
93
87
|
init_globals.update(_module_fqn=module_fqn, _modlib_path=modlib_path)
|
94
88
|
|
95
|
-
|
96
|
-
|
97
|
-
runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
|
89
|
+
# Run the module. By importing it as '__main__', it executes as a script.
|
90
|
+
runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
|
98
91
|
|
99
|
-
|
100
|
-
|
92
|
+
# An Ansible module must print its own results and exit. If execution reaches this point, that did not happen.
|
93
|
+
raise RuntimeError('New-style module did not handle its own exit.')
|
101
94
|
|
102
95
|
|
103
96
|
def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
|
@@ -112,22 +105,3 @@ def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
|
|
112
105
|
print(json.dumps(result, cls=encoder)) # pylint: disable=ansible-bad-function
|
113
106
|
|
114
107
|
sys.exit(1) # pylint: disable=ansible-bad-function
|
115
|
-
|
116
|
-
|
117
|
-
@dataclasses.dataclass(frozen=True)
|
118
|
-
class _ModulePluginWrapper(HasPluginInfo):
|
119
|
-
"""Modules aren't plugin instances; this adapter implements the `HasPluginInfo` protocol to allow `PluginExecContext` infra to work with modules."""
|
120
|
-
|
121
|
-
plugin: PluginInfo
|
122
|
-
|
123
|
-
@property
|
124
|
-
def _load_name(self) -> str:
|
125
|
-
return self.plugin.requested_name
|
126
|
-
|
127
|
-
@property
|
128
|
-
def ansible_name(self) -> str:
|
129
|
-
return self.plugin.resolved_name
|
130
|
-
|
131
|
-
@property
|
132
|
-
def plugin_type(self) -> str:
|
133
|
-
return self.plugin.type
|
@@ -1,7 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import dataclasses
|
4
|
-
import datetime
|
5
4
|
import typing as t
|
6
5
|
|
7
6
|
from ansible.module_utils.common import messages as _messages
|
@@ -12,27 +11,6 @@ from ansible.module_utils._internal import _datatag
|
|
12
11
|
class Deprecated(_datatag.AnsibleDatatagBase):
|
13
12
|
msg: str
|
14
13
|
help_text: t.Optional[str] = None
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@classmethod
|
20
|
-
def _from_dict(cls, d: t.Dict[str, t.Any]) -> Deprecated:
|
21
|
-
source = d
|
22
|
-
removal_date = source.get('removal_date')
|
23
|
-
|
24
|
-
if removal_date is not None:
|
25
|
-
source = source.copy()
|
26
|
-
source['removal_date'] = datetime.date.fromisoformat(removal_date)
|
27
|
-
|
28
|
-
return cls(**source)
|
29
|
-
|
30
|
-
def _as_dict(self) -> t.Dict[str, t.Any]:
|
31
|
-
# deprecated: description='no-args super() with slotted dataclass requires 3.14+' python_version='3.13'
|
32
|
-
# see: https://github.com/python/cpython/pull/124455
|
33
|
-
value = super(Deprecated, self)._as_dict()
|
34
|
-
|
35
|
-
if self.removal_date is not None:
|
36
|
-
value['removal_date'] = self.removal_date.isoformat()
|
37
|
-
|
38
|
-
return value
|
14
|
+
date: t.Optional[str] = None
|
15
|
+
version: t.Optional[str] = None
|
16
|
+
deprecator: t.Optional[_messages.PluginInfo] = None
|
@@ -0,0 +1,134 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import inspect
|
4
|
+
import re
|
5
|
+
import pathlib
|
6
|
+
import sys
|
7
|
+
import typing as t
|
8
|
+
|
9
|
+
from ansible.module_utils.common.messages import PluginInfo
|
10
|
+
|
11
|
+
_ansible_module_base_path: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
12
|
+
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
13
|
+
|
14
|
+
ANSIBLE_CORE_DEPRECATOR: t.Final = PluginInfo._from_collection_name('ansible.builtin')
|
15
|
+
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
|
16
|
+
|
17
|
+
INDETERMINATE_DEPRECATOR: t.Final = PluginInfo(resolved_name='indeterminate', type='indeterminate')
|
18
|
+
"""Singleton `PluginInfo` instance for indeterminate deprecator."""
|
19
|
+
|
20
|
+
_DEPRECATOR_PLUGIN_TYPES = frozenset(
|
21
|
+
{
|
22
|
+
'action',
|
23
|
+
'become',
|
24
|
+
'cache',
|
25
|
+
'callback',
|
26
|
+
'cliconf',
|
27
|
+
'connection',
|
28
|
+
# doc_fragments - no code execution
|
29
|
+
# filter - basename inadequate to identify plugin
|
30
|
+
'httpapi',
|
31
|
+
'inventory',
|
32
|
+
'lookup',
|
33
|
+
'module', # only for collections
|
34
|
+
'netconf',
|
35
|
+
'shell',
|
36
|
+
'strategy',
|
37
|
+
'terminal',
|
38
|
+
# test - basename inadequate to identify plugin
|
39
|
+
'vars',
|
40
|
+
}
|
41
|
+
)
|
42
|
+
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
43
|
+
|
44
|
+
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES = frozenset(
|
45
|
+
{
|
46
|
+
'filter',
|
47
|
+
'test',
|
48
|
+
}
|
49
|
+
)
|
50
|
+
"""Plugin types for which basename cannot be used to identify the plugin name."""
|
51
|
+
|
52
|
+
|
53
|
+
def get_best_deprecator(*, deprecator: PluginInfo | None = None, collection_name: str | None = None) -> PluginInfo:
|
54
|
+
"""Return the best-available `PluginInfo` for the caller of this method."""
|
55
|
+
_skip_stackwalk = True
|
56
|
+
|
57
|
+
if deprecator and collection_name:
|
58
|
+
raise ValueError('Specify only one of `deprecator` or `collection_name`.')
|
59
|
+
|
60
|
+
return deprecator or PluginInfo._from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
|
61
|
+
|
62
|
+
|
63
|
+
def get_caller_plugin_info() -> PluginInfo | None:
|
64
|
+
"""Try to get `PluginInfo` for the caller of this method, ignoring marked infrastructure stack frames."""
|
65
|
+
_skip_stackwalk = True
|
66
|
+
|
67
|
+
if frame_info := next((frame_info for frame_info in inspect.stack() if '_skip_stackwalk' not in frame_info.frame.f_locals), None):
|
68
|
+
return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
|
69
|
+
|
70
|
+
return None # pragma: nocover
|
71
|
+
|
72
|
+
|
73
|
+
def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
74
|
+
"""Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
|
75
|
+
try:
|
76
|
+
relpath = str(pathlib.Path(path).relative_to(_ansible_module_base_path))
|
77
|
+
except ValueError:
|
78
|
+
return None # not ansible-core
|
79
|
+
|
80
|
+
namespace = 'ansible.builtin'
|
81
|
+
|
82
|
+
if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
|
83
|
+
plugin_name = match.group("plugin_name")
|
84
|
+
plugin_type = match.group("plugin_type")
|
85
|
+
|
86
|
+
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
87
|
+
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
88
|
+
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
|
89
|
+
# Callers in this case need to identify the deprecating plugin name, otherwise only ansible-core will be reported.
|
90
|
+
# Reporting ansible-core is never wrong, it just may be missing an additional detail (plugin name) in the "on behalf of" case.
|
91
|
+
return ANSIBLE_CORE_DEPRECATOR
|
92
|
+
elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
|
93
|
+
# AnsiballZ Python package for core modules
|
94
|
+
plugin_name = match.group("module_name")
|
95
|
+
plugin_type = "module"
|
96
|
+
elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
|
97
|
+
# AnsiballZ Python package for non-core library/role modules
|
98
|
+
namespace = 'ansible.legacy'
|
99
|
+
|
100
|
+
plugin_name = match.group("module_name")
|
101
|
+
plugin_type = "module"
|
102
|
+
else:
|
103
|
+
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
|
104
|
+
|
105
|
+
name = f'{namespace}.{plugin_name}'
|
106
|
+
|
107
|
+
return PluginInfo(resolved_name=name, type=plugin_type)
|
108
|
+
|
109
|
+
|
110
|
+
def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
111
|
+
"""Return a `PluginInfo` instance if the provided `path` refers to a collection plugin."""
|
112
|
+
if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
|
113
|
+
return None
|
114
|
+
|
115
|
+
plugin_type = match.group('plugin_type')
|
116
|
+
|
117
|
+
if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
|
118
|
+
# We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
|
119
|
+
# To keep things simple we'll fall back to just identifying the namespace and collection.
|
120
|
+
# In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
|
121
|
+
return PluginInfo._from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
|
122
|
+
|
123
|
+
if plugin_type == 'modules':
|
124
|
+
plugin_type = 'module'
|
125
|
+
|
126
|
+
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
127
|
+
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
128
|
+
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
|
129
|
+
# Callers in this case need to identify the deprecator to avoid ambiguity, since it could be the same collection or another collection.
|
130
|
+
return INDETERMINATE_DEPRECATOR
|
131
|
+
|
132
|
+
name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
|
133
|
+
|
134
|
+
return PluginInfo(resolved_name=name, type=plugin_type)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import typing as t
|
4
|
+
|
5
|
+
from ..common import messages as _messages
|
6
|
+
|
7
|
+
|
8
|
+
class HasPluginInfo(t.Protocol):
|
9
|
+
"""Protocol to type-annotate and expose PluginLoader-set values."""
|
10
|
+
|
11
|
+
@property
|
12
|
+
def ansible_name(self) -> str | None:
|
13
|
+
"""Fully resolved plugin name."""
|
14
|
+
|
15
|
+
@property
|
16
|
+
def plugin_type(self) -> str:
|
17
|
+
"""Plugin type name."""
|
18
|
+
|
19
|
+
|
20
|
+
def get_plugin_info(value: HasPluginInfo) -> _messages.PluginInfo:
|
21
|
+
"""Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol."""
|
22
|
+
return _messages.PluginInfo(
|
23
|
+
resolved_name=value.ansible_name,
|
24
|
+
type=value.plugin_type,
|
25
|
+
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import keyword
|
4
|
+
|
5
|
+
|
6
|
+
def validate_collection_name(collection_name: object, name: str = 'collection_name') -> None:
|
7
|
+
"""Validate a collection name."""
|
8
|
+
if not isinstance(collection_name, str):
|
9
|
+
raise TypeError(f"{name} must be {str} instead of {type(collection_name)}")
|
10
|
+
|
11
|
+
parts = collection_name.split('.')
|
12
|
+
|
13
|
+
if len(parts) != 2 or not all(part.isidentifier() and not keyword.iskeyword(part) for part in parts):
|
14
|
+
raise ValueError(f"{name} must consist of two non-keyword identifiers separated by '.'")
|
ansible/module_utils/basic.py
CHANGED
@@ -75,7 +75,7 @@ except ImportError:
|
|
75
75
|
# Python2 & 3 way to get NoneType
|
76
76
|
NoneType = type(None)
|
77
77
|
|
78
|
-
from ._internal import _traceback, _errors, _debugging
|
78
|
+
from ._internal import _traceback, _errors, _debugging, _deprecator
|
79
79
|
|
80
80
|
from .common.text.converters import (
|
81
81
|
to_native,
|
@@ -509,16 +509,31 @@ class AnsibleModule(object):
|
|
509
509
|
warn(warning)
|
510
510
|
self.log('[WARNING] %s' % warning)
|
511
511
|
|
512
|
-
def deprecate(
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
512
|
+
def deprecate(
|
513
|
+
self,
|
514
|
+
msg: str,
|
515
|
+
version: str | None = None,
|
516
|
+
date: str | None = None,
|
517
|
+
collection_name: str | None = None,
|
518
|
+
*,
|
519
|
+
deprecator: _messages.PluginInfo | None = None,
|
520
|
+
help_text: str | None = None,
|
521
|
+
) -> None:
|
522
|
+
"""
|
523
|
+
Record a deprecation warning to be returned with the module result.
|
524
|
+
Most callers do not need to provide `collection_name` or `deprecator` -- but provide only one if needed.
|
525
|
+
Specify `version` or `date`, but not both.
|
526
|
+
If `date` is a string, it must be in the form `YYYY-MM-DD`.
|
527
|
+
"""
|
528
|
+
_skip_stackwalk = True
|
529
|
+
|
530
|
+
deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
|
531
|
+
msg=msg,
|
532
|
+
version=version,
|
533
|
+
date=date,
|
534
|
+
deprecator=_deprecator.get_best_deprecator(deprecator=deprecator, collection_name=collection_name),
|
535
|
+
help_text=help_text,
|
536
|
+
)
|
522
537
|
|
523
538
|
def load_file_common_arguments(self, params, path=None):
|
524
539
|
"""
|
@@ -1404,6 +1419,7 @@ class AnsibleModule(object):
|
|
1404
1419
|
self.cleanup(path)
|
1405
1420
|
|
1406
1421
|
def _return_formatted(self, kwargs):
|
1422
|
+
_skip_stackwalk = True
|
1407
1423
|
|
1408
1424
|
self.add_path_info(kwargs)
|
1409
1425
|
|
@@ -1411,6 +1427,13 @@ class AnsibleModule(object):
|
|
1411
1427
|
kwargs['invocation'] = {'module_args': self.params}
|
1412
1428
|
|
1413
1429
|
if 'warnings' in kwargs:
|
1430
|
+
self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name
|
1431
|
+
msg='Passing `warnings` to `exit_json` or `fail_json` is deprecated.',
|
1432
|
+
version='2.23',
|
1433
|
+
help_text='Use `AnsibleModule.warn` instead.',
|
1434
|
+
deprecator=_deprecator.ANSIBLE_CORE_DEPRECATOR,
|
1435
|
+
)
|
1436
|
+
|
1414
1437
|
if isinstance(kwargs['warnings'], list):
|
1415
1438
|
for w in kwargs['warnings']:
|
1416
1439
|
self.warn(w)
|
@@ -1422,17 +1445,38 @@ class AnsibleModule(object):
|
|
1422
1445
|
kwargs['warnings'] = warnings
|
1423
1446
|
|
1424
1447
|
if 'deprecations' in kwargs:
|
1448
|
+
self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name
|
1449
|
+
msg='Passing `deprecations` to `exit_json` or `fail_json` is deprecated.',
|
1450
|
+
version='2.23',
|
1451
|
+
help_text='Use `AnsibleModule.deprecate` instead.',
|
1452
|
+
deprecator=_deprecator.ANSIBLE_CORE_DEPRECATOR,
|
1453
|
+
)
|
1454
|
+
|
1425
1455
|
if isinstance(kwargs['deprecations'], list):
|
1426
1456
|
for d in kwargs['deprecations']:
|
1427
|
-
if isinstance(d,
|
1428
|
-
self.deprecate(
|
1457
|
+
if isinstance(d, (KeysView, Sequence)) and len(d) == 2:
|
1458
|
+
self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-invalid-deprecated-version
|
1459
|
+
msg=d[0],
|
1460
|
+
version=d[1],
|
1461
|
+
deprecator=_deprecator.get_best_deprecator(),
|
1462
|
+
)
|
1429
1463
|
elif isinstance(d, Mapping):
|
1430
|
-
self.deprecate(
|
1431
|
-
|
1464
|
+
self.deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
|
1465
|
+
msg=d['msg'],
|
1466
|
+
version=d.get('version'),
|
1467
|
+
date=d.get('date'),
|
1468
|
+
deprecator=_deprecator.get_best_deprecator(collection_name=d.get('collection_name')),
|
1469
|
+
)
|
1432
1470
|
else:
|
1433
|
-
self.deprecate(
|
1471
|
+
self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-deprecated-no-version
|
1472
|
+
msg=d,
|
1473
|
+
deprecator=_deprecator.get_best_deprecator(),
|
1474
|
+
)
|
1434
1475
|
else:
|
1435
|
-
self.deprecate(
|
1476
|
+
self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-deprecated-no-version
|
1477
|
+
msg=kwargs['deprecations'],
|
1478
|
+
deprecator=_deprecator.get_best_deprecator(),
|
1479
|
+
)
|
1436
1480
|
|
1437
1481
|
deprecations = get_deprecations()
|
1438
1482
|
if deprecations:
|
@@ -1452,6 +1496,7 @@ class AnsibleModule(object):
|
|
1452
1496
|
|
1453
1497
|
def exit_json(self, **kwargs) -> t.NoReturn:
|
1454
1498
|
""" return from the module, without error """
|
1499
|
+
_skip_stackwalk = True
|
1455
1500
|
|
1456
1501
|
self.do_cleanup_files()
|
1457
1502
|
self._return_formatted(kwargs)
|
@@ -1473,6 +1518,8 @@ class AnsibleModule(object):
|
|
1473
1518
|
When `exception` is not specified, a formatted traceback will be retrieved from the current exception.
|
1474
1519
|
If no exception is pending, the current call stack will be used instead.
|
1475
1520
|
"""
|
1521
|
+
_skip_stackwalk = True
|
1522
|
+
|
1476
1523
|
msg = str(msg) # coerce to str instead of raising an error due to an invalid type
|
1477
1524
|
|
1478
1525
|
kwargs.update(
|
@@ -22,6 +22,7 @@ from ansible.module_utils.common.parameters import (
|
|
22
22
|
|
23
23
|
from ansible.module_utils.common.text.converters import to_native
|
24
24
|
from ansible.module_utils.common.warnings import deprecate, warn
|
25
|
+
from ansible.module_utils.common import messages as _messages
|
25
26
|
|
26
27
|
from ansible.module_utils.common.validation import (
|
27
28
|
check_mutually_exclusive,
|
@@ -300,9 +301,13 @@ class ModuleArgumentSpecValidator(ArgumentSpecValidator):
|
|
300
301
|
result = super(ModuleArgumentSpecValidator, self).validate(parameters)
|
301
302
|
|
302
303
|
for d in result._deprecations:
|
303
|
-
|
304
|
-
|
305
|
-
|
304
|
+
# DTFIX-FUTURE: pass an actual deprecator instead of one derived from collection_name
|
305
|
+
deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
|
306
|
+
msg=d['msg'],
|
307
|
+
version=d.get('version'),
|
308
|
+
date=d.get('date'),
|
309
|
+
deprecator=_messages.PluginInfo._from_collection_name(d.get('collection_name')),
|
310
|
+
)
|
306
311
|
|
307
312
|
for w in result._warnings:
|
308
313
|
warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))
|
@@ -13,7 +13,7 @@ import dataclasses as _dataclasses
|
|
13
13
|
# deprecated: description='typing.Self exists in Python 3.11+' python_version='3.10'
|
14
14
|
from ..compat import typing as _t
|
15
15
|
|
16
|
-
from ansible.module_utils._internal import _datatag
|
16
|
+
from ansible.module_utils._internal import _datatag, _validation
|
17
17
|
|
18
18
|
if _sys.version_info >= (3, 10):
|
19
19
|
# Using slots for reduced memory usage and improved performance.
|
@@ -27,13 +27,27 @@ else:
|
|
27
27
|
class PluginInfo(_datatag.AnsibleSerializableDataclass):
|
28
28
|
"""Information about a loaded plugin."""
|
29
29
|
|
30
|
-
requested_name: str
|
31
|
-
"""The plugin name as requested, before resolving, which may be partially or fully qualified."""
|
32
30
|
resolved_name: str
|
33
31
|
"""The resolved canonical plugin name; always fully-qualified for collection plugins."""
|
34
32
|
type: str
|
35
33
|
"""The plugin type."""
|
36
34
|
|
35
|
+
_COLLECTION_ONLY_TYPE: _t.ClassVar[str] = 'collection'
|
36
|
+
"""This is not a real plugin type. It's a placeholder for use by a `PluginInfo` instance which references a collection without a plugin."""
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def _from_collection_name(cls, collection_name: str | None) -> _t.Self | None:
|
40
|
+
"""Returns an instance with the special `collection` type to refer to a non-plugin or ambiguous caller within a collection."""
|
41
|
+
if not collection_name:
|
42
|
+
return None
|
43
|
+
|
44
|
+
_validation.validate_collection_name(collection_name)
|
45
|
+
|
46
|
+
return cls(
|
47
|
+
resolved_name=collection_name,
|
48
|
+
type=cls._COLLECTION_ONLY_TYPE,
|
49
|
+
)
|
50
|
+
|
37
51
|
|
38
52
|
@_dataclasses.dataclass(**_dataclass_kwargs)
|
39
53
|
class Detail(_datatag.AnsibleSerializableDataclass):
|
@@ -75,34 +89,37 @@ class WarningSummary(SummaryBase):
|
|
75
89
|
class DeprecationSummary(WarningSummary):
|
76
90
|
"""Deprecation summary with details (possibly derived from an exception __cause__ chain) and an optional traceback."""
|
77
91
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@property
|
83
|
-
def collection_name(self) -> _t.Optional[str]:
|
84
|
-
if not self.plugin:
|
85
|
-
return None
|
86
|
-
|
87
|
-
parts = self.plugin.resolved_name.split('.')
|
88
|
-
|
89
|
-
if len(parts) < 2:
|
90
|
-
return None
|
91
|
-
|
92
|
-
collection_name = '.'.join(parts[:2])
|
92
|
+
deprecator: _t.Optional[PluginInfo] = None
|
93
|
+
"""
|
94
|
+
The identifier for the content which is being deprecated.
|
95
|
+
"""
|
93
96
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
+
date: _t.Optional[str] = None
|
98
|
+
"""
|
99
|
+
The date after which a new release of `deprecator` will remove the feature described by `msg`.
|
100
|
+
Ignored if `deprecator` is not provided.
|
101
|
+
"""
|
97
102
|
|
98
|
-
|
103
|
+
version: _t.Optional[str] = None
|
104
|
+
"""
|
105
|
+
The version of `deprecator` which will remove the feature described by `msg`.
|
106
|
+
Ignored if `deprecator` is not provided.
|
107
|
+
Ignored if `date` is provided.
|
108
|
+
"""
|
99
109
|
|
100
110
|
def _as_simple_dict(self) -> _t.Dict[str, _t.Any]:
|
101
111
|
"""Returns a dictionary representation of the deprecation object in the format exposed to playbooks."""
|
112
|
+
from ansible.module_utils._internal._deprecator import INDETERMINATE_DEPRECATOR # circular import from messages
|
113
|
+
|
114
|
+
if self.deprecator and self.deprecator != INDETERMINATE_DEPRECATOR:
|
115
|
+
collection_name = '.'.join(self.deprecator.resolved_name.split('.')[:2])
|
116
|
+
else:
|
117
|
+
collection_name = None
|
118
|
+
|
102
119
|
result = self._as_dict()
|
103
120
|
result.update(
|
104
121
|
msg=self._format(),
|
105
|
-
collection_name=
|
122
|
+
collection_name=collection_name,
|
106
123
|
)
|
107
124
|
|
108
125
|
return result
|
@@ -3,14 +3,12 @@
|
|
3
3
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
|
-
import dataclasses
|
7
6
|
import os
|
8
7
|
import pathlib
|
9
8
|
import subprocess
|
10
9
|
import sys
|
11
10
|
import typing as t
|
12
11
|
|
13
|
-
from ansible.module_utils._internal import _plugin_exec_context
|
14
12
|
from ansible.module_utils.common.text.converters import to_bytes
|
15
13
|
|
16
14
|
_ANSIBLE_PARENT_PATH = pathlib.Path(__file__).parents[3]
|
@@ -99,7 +97,6 @@ if __name__ == '__main__':
|
|
99
97
|
|
100
98
|
json_params = {json_params!r}
|
101
99
|
profile = {profile!r}
|
102
|
-
plugin_info_dict = {plugin_info_dict!r}
|
103
100
|
module_fqn = {module_fqn!r}
|
104
101
|
modlib_path = {modlib_path!r}
|
105
102
|
|
@@ -110,19 +107,15 @@ if __name__ == '__main__':
|
|
110
107
|
_ansiballz.run_module(
|
111
108
|
json_params=json_params,
|
112
109
|
profile=profile,
|
113
|
-
plugin_info_dict=plugin_info_dict,
|
114
110
|
module_fqn=module_fqn,
|
115
111
|
modlib_path=modlib_path,
|
116
112
|
init_globals=dict(_respawned=True),
|
117
113
|
)
|
118
114
|
"""
|
119
115
|
|
120
|
-
plugin_info = _plugin_exec_context.PluginExecContext.get_current_plugin_info()
|
121
|
-
|
122
116
|
respawn_code = respawn_code_template.format(
|
123
117
|
json_params=basic._ANSIBLE_ARGS,
|
124
118
|
profile=basic._ANSIBLE_PROFILE,
|
125
|
-
plugin_info_dict=dataclasses.asdict(plugin_info),
|
126
119
|
module_fqn=module_fqn,
|
127
120
|
modlib_path=modlib_path,
|
128
121
|
)
|