ansible-core 2.19.0b3__py3-none-any.whl → 2.19.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansible/_internal/__init__.py +2 -2
- ansible/_internal/_collection_proxy.py +1 -1
- ansible/_internal/_errors/_alarm_timeout.py +66 -0
- ansible/_internal/_errors/_captured.py +25 -30
- ansible/_internal/_errors/_error_factory.py +89 -0
- ansible/_internal/_errors/_error_utils.py +240 -0
- ansible/_internal/_errors/_task_timeout.py +28 -0
- ansible/_internal/_event_formatting.py +127 -0
- ansible/_internal/_json/__init__.py +6 -6
- ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
- ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
- ansible/_internal/_json/_profiles/_legacy.py +3 -11
- ansible/_internal/_ssh/__init__.py +0 -0
- ansible/_internal/_ssh/_agent_launch.py +91 -0
- ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
- ansible/_internal/_templating/__init__.py +5 -3
- ansible/_internal/_templating/_datatag.py +2 -1
- ansible/_internal/_templating/_engine.py +3 -4
- ansible/_internal/_templating/_jinja_bits.py +21 -16
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +31 -3
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_transform.py +20 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_testing.py +26 -0
- ansible/_internal/_yaml/_dumper.py +1 -1
- ansible/_internal/_yaml/_errors.py +7 -7
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
- ansible/cli/__init__.py +5 -82
- ansible/cli/arguments/option_helpers.py +8 -5
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +27 -23
- ansible/config/manager.py +142 -101
- ansible/constants.py +1 -1
- ansible/errors/__init__.py +96 -49
- ansible/executor/module_common.py +8 -10
- ansible/executor/powershell/async_watchdog.ps1 +2 -2
- ansible/executor/powershell/async_wrapper.ps1 +3 -3
- ansible/executor/powershell/become_wrapper.ps1 +20 -2
- ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
- ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
- ansible/executor/powershell/exec_wrapper.ps1 +219 -6
- ansible/executor/powershell/module_manifest.py +52 -0
- ansible/executor/powershell/module_wrapper.ps1 +47 -21
- ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
- ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
- ansible/executor/process/worker.py +38 -113
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/collection/__init__.py +1 -4
- ansible/inventory/manager.py +1 -0
- ansible/module_utils/_internal/__init__.py +0 -3
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz.py +4 -2
- ansible/module_utils/_internal/_datatag/__init__.py +20 -14
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +66 -48
- ansible/module_utils/_internal/_errors.py +88 -17
- ansible/module_utils/_internal/_event_utils.py +61 -0
- ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
- ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +1 -1
- ansible/module_utils/_internal/_stack.py +22 -0
- ansible/module_utils/_internal/_text_utils.py +6 -0
- ansible/module_utils/_internal/_traceback.py +11 -8
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +49 -15
- ansible/module_utils/common/arg_spec.py +2 -2
- ansible/module_utils/common/collections.py +6 -0
- ansible/module_utils/common/json.py +2 -2
- ansible/module_utils/common/text/converters.py +3 -3
- ansible/module_utils/common/validation.py +1 -1
- ansible/module_utils/common/warnings.py +80 -23
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +15 -22
- ansible/modules/git.py +1 -6
- ansible/modules/hostname.py +0 -1
- ansible/modules/pip.py +2 -4
- ansible/modules/service.py +3 -9
- ansible/modules/sysvinit.py +3 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +4 -4
- ansible/parsing/mod_args.py +1 -1
- ansible/parsing/plugin_docs.py +2 -2
- ansible/parsing/utils/yaml.py +3 -3
- ansible/parsing/vault/__init__.py +4 -4
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +0 -3
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +9 -32
- ansible/plugins/action/add_host.py +1 -1
- ansible/plugins/action/assemble.py +8 -16
- ansible/plugins/action/async_status.py +7 -2
- ansible/plugins/action/copy.py +8 -7
- ansible/plugins/action/gather_facts.py +8 -8
- ansible/plugins/action/package.py +5 -8
- ansible/plugins/action/script.py +8 -15
- ansible/plugins/action/service.py +3 -7
- ansible/plugins/action/template.py +6 -8
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/connection/local.py +2 -2
- ansible/plugins/connection/ssh.py +17 -9
- ansible/plugins/connection/winrm.py +5 -2
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +13 -6
- ansible/plugins/filter/encryption.py +4 -4
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +69 -16
- ansible/plugins/loader.py +10 -9
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +66 -9
- ansible/plugins/shell/sh.py +9 -5
- ansible/plugins/test/core.py +21 -15
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +30 -2
- ansible/utils/collection_loader/__init__.py +2 -0
- ansible/utils/display.py +107 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/docker.txt +3 -3
- ansible_test/_data/completion/remote.txt +1 -0
- ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/util.py +20 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -280,8 +280,6 @@ class CallbackBase(AnsiblePlugin):
|
|
280
280
|
# that want to further modify the result, or use custom serialization
|
281
281
|
return abridged_result
|
282
282
|
|
283
|
-
# DTFIX-RELEASE: Switch to stock json/yaml serializers here? We should always have a transformed plain-types result.
|
284
|
-
|
285
283
|
if result_format == 'json':
|
286
284
|
return json.dumps(abridged_result, cls=_fallback_to_str.Encoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
|
287
285
|
|
@@ -310,7 +308,7 @@ class CallbackBase(AnsiblePlugin):
|
|
310
308
|
' ' * (indent or 4)
|
311
309
|
)
|
312
310
|
|
313
|
-
#
|
311
|
+
# DTFIX5: add test to exercise this case
|
314
312
|
raise ValueError(f'Unsupported result_format {result_format!r}.')
|
315
313
|
|
316
314
|
def _handle_warnings(self, res: _c.MutableMapping[str, t.Any]) -> None:
|
@@ -318,7 +316,7 @@ class CallbackBase(AnsiblePlugin):
|
|
318
316
|
if res.pop('warnings', None) and self._current_task_result and (warnings := self._current_task_result.warnings):
|
319
317
|
# display warnings from the current task result if `warnings` was not removed from `result` (or made falsey)
|
320
318
|
for warning in warnings:
|
321
|
-
#
|
319
|
+
# DTFIX3: what to do about propagating wrap_text from the original display.warning call?
|
322
320
|
self._display._warning(warning, wrap_text=False)
|
323
321
|
|
324
322
|
if res.pop('deprecations', None) and self._current_task_result and (deprecations := self._current_task_result.deprecations):
|
@@ -333,7 +331,7 @@ class CallbackBase(AnsiblePlugin):
|
|
333
331
|
|
334
332
|
def _handle_warnings_and_exception(self, result: CallbackTaskResult) -> None:
|
335
333
|
"""Standardized handling of warnings/deprecations and exceptions from a task/item result."""
|
336
|
-
#
|
334
|
+
# DTFIX5: make/doc/porting-guide a public version of this method?
|
337
335
|
try:
|
338
336
|
use_stderr = self.get_option('display_failed_stderr')
|
339
337
|
except KeyError:
|
@@ -374,7 +372,7 @@ class CallbackBase(AnsiblePlugin):
|
|
374
372
|
' '
|
375
373
|
)
|
376
374
|
|
377
|
-
#
|
375
|
+
# DTFIX5: add test to exercise this case
|
378
376
|
raise ValueError(f'Unsupported result_format {result_format!r}.')
|
379
377
|
|
380
378
|
def _get_diff(self, difflist):
|
@@ -90,6 +90,8 @@ import typing as t
|
|
90
90
|
|
91
91
|
from ansible import constants
|
92
92
|
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
93
|
+
from ansible.module_utils._internal import _event_utils
|
94
|
+
from ansible._internal import _event_formatting
|
93
95
|
from ansible.playbook.task import Task
|
94
96
|
from ansible.plugins.callback import CallbackBase
|
95
97
|
from ansible.executor.task_result import CallbackTaskResult
|
@@ -248,8 +250,8 @@ class CallbackModule(CallbackBase):
|
|
248
250
|
|
249
251
|
if host_data.status == 'failed':
|
250
252
|
if error_summary := task_result.exception:
|
251
|
-
message = error_summary.
|
252
|
-
output = error_summary.
|
253
|
+
message = _event_utils.format_event_brief_message(error_summary.event)
|
254
|
+
output = _event_formatting.format_event_traceback(error_summary.event)
|
253
255
|
test_case.errors.append(TestError(message=message, output=output))
|
254
256
|
elif 'msg' in res:
|
255
257
|
message = res['msg']
|
@@ -101,9 +101,9 @@ class Connection(ConnectionBase):
|
|
101
101
|
display.debug("opening command with Popen()")
|
102
102
|
|
103
103
|
if isinstance(cmd, (text_type, binary_type)):
|
104
|
-
cmd =
|
104
|
+
cmd = to_text(cmd)
|
105
105
|
else:
|
106
|
-
cmd = map(
|
106
|
+
cmd = map(to_text, cmd)
|
107
107
|
|
108
108
|
pty_primary = None
|
109
109
|
stdin = subprocess.PIPE
|
@@ -398,6 +398,17 @@ DOCUMENTATION = """
|
|
398
398
|
- {key: pkcs11_provider, section: ssh_connection}
|
399
399
|
vars:
|
400
400
|
- name: ansible_ssh_pkcs11_provider
|
401
|
+
verbosity:
|
402
|
+
version_added: '2.19'
|
403
|
+
default: 0
|
404
|
+
type: int
|
405
|
+
description:
|
406
|
+
- Requested verbosity level for the SSH CLI.
|
407
|
+
env: [{name: ANSIBLE_SSH_VERBOSITY}]
|
408
|
+
ini:
|
409
|
+
- {key: verbosity, section: ssh_connection}
|
410
|
+
vars:
|
411
|
+
- name: ansible_ssh_verbosity
|
401
412
|
"""
|
402
413
|
|
403
414
|
import collections.abc as c
|
@@ -436,7 +447,7 @@ from ansible.plugins.connection import ConnectionBase, BUFSIZE
|
|
436
447
|
from ansible.plugins.shell.powershell import _replace_stderr_clixml
|
437
448
|
from ansible.utils.display import Display
|
438
449
|
from ansible.utils.path import unfrackpath, makedirs_safe
|
439
|
-
from ansible.
|
450
|
+
from ansible._internal._ssh import _ssh_agent
|
440
451
|
|
441
452
|
try:
|
442
453
|
from cryptography.hazmat.primitives import serialization
|
@@ -755,12 +766,12 @@ class Connection(ConnectionBase):
|
|
755
766
|
key_data = self.get_option('private_key')
|
756
767
|
passphrase = self.get_option('private_key_passphrase')
|
757
768
|
|
758
|
-
private_key, public_key, fingerprint =
|
769
|
+
private_key, public_key, fingerprint = _ssh_agent.key_data_into_crypto_objects(
|
759
770
|
to_bytes(key_data),
|
760
771
|
to_bytes(passphrase) if passphrase else None,
|
761
772
|
)
|
762
773
|
|
763
|
-
with SshAgentClient(auth_sock) as client:
|
774
|
+
with _ssh_agent.SshAgentClient(auth_sock) as client:
|
764
775
|
if public_key not in client:
|
765
776
|
display.vvv(f'SSH: SSH_AGENT adding {fingerprint} to agent', host=self.host)
|
766
777
|
client.add(
|
@@ -855,8 +866,8 @@ class Connection(ConnectionBase):
|
|
855
866
|
self._add_args(b_command, b_args, u'disable batch mode for password auth')
|
856
867
|
b_command += [b'-b', b'-']
|
857
868
|
|
858
|
-
if
|
859
|
-
b_command.append(b'-' + (b'v' *
|
869
|
+
if (verbosity := self.get_option('verbosity')) > 0:
|
870
|
+
b_command.append(b'-' + (b'v' * verbosity))
|
860
871
|
|
861
872
|
# Next, we add ssh_args
|
862
873
|
ssh_args = self.get_option('ssh_args')
|
@@ -879,10 +890,7 @@ class Connection(ConnectionBase):
|
|
879
890
|
try:
|
880
891
|
key = self._populate_agent()
|
881
892
|
except Exception as e:
|
882
|
-
raise AnsibleAuthenticationFailure(
|
883
|
-
'Failed to add configured private key into ssh-agent',
|
884
|
-
orig_exc=e,
|
885
|
-
)
|
893
|
+
raise AnsibleAuthenticationFailure('Failed to add configured private key into ssh-agent.') from e
|
886
894
|
b_args = (b'-o', b'IdentitiesOnly=yes', b'-o', to_bytes(f'IdentityFile="{key}"', errors='surrogate_or_strict'))
|
887
895
|
self._add_args(b_command, b_args, "ANSIBLE_PRIVATE_KEY/private_key set")
|
888
896
|
elif key := self.get_option('private_key_file'):
|
@@ -723,8 +723,11 @@ class Connection(ConnectionBase):
|
|
723
723
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
724
724
|
|
725
725
|
encoded_prefix = self._shell._encode_script('', as_list=False, strict_mode=False, preserve_rc=False)
|
726
|
-
if cmd.startswith(encoded_prefix):
|
727
|
-
# Avoid double encoding the script
|
726
|
+
if cmd.startswith(encoded_prefix) or cmd.startswith("type "):
|
727
|
+
# Avoid double encoding the script, the first means we are already
|
728
|
+
# running the standard PowerShell command, the latter is used for
|
729
|
+
# the no pipeline case where it uses type to pipe the script into
|
730
|
+
# powershell which is known to work without re-encoding as pwsh.
|
728
731
|
cmd_parts = cmd.split(" ")
|
729
732
|
else:
|
730
733
|
cmd_parts = self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)
|
@@ -47,13 +47,13 @@ options:
|
|
47
47
|
- The key from input dictionary used to generate groups.
|
48
48
|
default_value:
|
49
49
|
description:
|
50
|
-
- The default value when the host variable's value is an empty string.
|
50
|
+
- The default value when the host variable's value is V(None) or an empty string.
|
51
51
|
- This option is mutually exclusive with O(keyed_groups[].trailing_separator).
|
52
52
|
type: str
|
53
53
|
version_added: '2.12'
|
54
54
|
trailing_separator:
|
55
55
|
description:
|
56
|
-
- Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is an empty string.
|
56
|
+
- Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is V(None) or an empty string.
|
57
57
|
- This option is mutually exclusive with O(keyed_groups[].default_value).
|
58
58
|
type: bool
|
59
59
|
default: true
|
ansible/plugins/filter/core.py
CHANGED
@@ -34,7 +34,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
|
|
34
34
|
from ansible.module_utils.common.collections import is_sequence
|
35
35
|
from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
|
36
36
|
from ansible.parsing.yaml.dumper import AnsibleDumper
|
37
|
-
from ansible.
|
37
|
+
from ansible.template import accept_args_markers, accept_lazy_markers
|
38
38
|
from ansible._internal._templating._jinja_common import MarkerError, UndefinedMarker, validate_arg_type
|
39
39
|
from ansible.utils.display import Display
|
40
40
|
from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
|
@@ -247,20 +247,30 @@ def regex_escape(string, re_type='python'):
|
|
247
247
|
|
248
248
|
|
249
249
|
def from_yaml(data):
|
250
|
+
if data is None:
|
251
|
+
return None
|
252
|
+
|
250
253
|
if isinstance(data, string_types):
|
251
254
|
# The ``text_type`` call here strips any custom
|
252
255
|
# string wrapper class, so that CSafeLoader can
|
253
256
|
# read the data
|
254
257
|
return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
|
258
|
+
|
259
|
+
display.deprecated(f"The from_yaml filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
|
255
260
|
return data
|
256
261
|
|
257
262
|
|
258
263
|
def from_yaml_all(data):
|
264
|
+
if data is None:
|
265
|
+
return [] # backward compatibility; ensure consistent result between classic/native Jinja for None/empty string input
|
266
|
+
|
259
267
|
if isinstance(data, string_types):
|
260
268
|
# The ``text_type`` call here strips any custom
|
261
269
|
# string wrapper class, so that CSafeLoader can
|
262
270
|
# read the data
|
263
271
|
return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
|
272
|
+
|
273
|
+
display.deprecated(f"The from_yaml_all filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
|
264
274
|
return data
|
265
275
|
|
266
276
|
|
@@ -338,7 +348,7 @@ def to_uuid(string, namespace=UUID_NAMESPACE_ANSIBLE):
|
|
338
348
|
@accept_args_markers
|
339
349
|
def mandatory(a: object, msg: str | None = None) -> object:
|
340
350
|
"""Make a variable mandatory."""
|
341
|
-
# DTFIX-
|
351
|
+
# DTFIX-FUTURE: deprecate this filter; there are much better ways via undef, etc...
|
342
352
|
# also remember to remove unit test checking for _undefined_name
|
343
353
|
if isinstance(a, UndefinedMarker):
|
344
354
|
if msg is not None:
|
@@ -654,7 +664,7 @@ def _cleansed_groupby(*args, **kwargs):
|
|
654
664
|
|
655
665
|
return res
|
656
666
|
|
657
|
-
# DTFIX-
|
667
|
+
# DTFIX-FUTURE: make these dumb wrappers more dynamic
|
658
668
|
|
659
669
|
|
660
670
|
@accept_args_markers
|
@@ -806,7 +816,6 @@ class FilterModule(object):
|
|
806
816
|
'groupby': _cleansed_groupby,
|
807
817
|
|
808
818
|
# Jinja builtins that need special arg handling
|
809
|
-
# DTFIX-RELEASE: document these now that they're overridden, or hide them so they don't show up as undocumented
|
810
819
|
'd': ansible_default, # replaces the implementation instead of wrapping it
|
811
820
|
'default': ansible_default, # replaces the implementation instead of wrapping it
|
812
821
|
'map': wrapped_map,
|
@@ -815,5 +824,3 @@ class FilterModule(object):
|
|
815
824
|
'reject': wrapped_reject,
|
816
825
|
'rejectattr': wrapped_rejectattr,
|
817
826
|
}
|
818
|
-
|
819
|
-
# DTFIX-RELEASE: document protomatter plugins, or hide them from ansible-doc/galaxy (not related to this code, but needed some place to put this comment)
|
@@ -4,10 +4,10 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from ansible.errors import AnsibleError
|
6
6
|
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
7
|
-
from ansible.
|
8
|
-
from ansible._internal._templating._jinja_common import get_first_marker_arg, VaultExceptionMarker
|
7
|
+
from ansible._internal._templating._jinja_common import VaultExceptionMarker
|
9
8
|
from ansible._internal._datatag._tags import VaultedValue
|
10
9
|
from ansible.parsing.vault import is_encrypted, VaultSecret, VaultLib, VaultHelper
|
10
|
+
from ansible import template as _template
|
11
11
|
from ansible.utils.display import Display
|
12
12
|
|
13
13
|
display = Display()
|
@@ -43,12 +43,12 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
|
|
43
43
|
return vault
|
44
44
|
|
45
45
|
|
46
|
-
@accept_args_markers
|
46
|
+
@_template.accept_args_markers
|
47
47
|
def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
|
48
48
|
if isinstance(vault, VaultExceptionMarker):
|
49
49
|
vault = vault._disarm()
|
50
50
|
|
51
|
-
if (first_marker := get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
|
51
|
+
if (first_marker := _template.get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
|
52
52
|
return first_marker
|
53
53
|
|
54
54
|
if not isinstance(secret, (str, bytes)):
|
@@ -334,7 +334,6 @@ class Cacheable(_plugin_info.HasPluginInfo, _ConfigurablePlugin):
|
|
334
334
|
|
335
335
|
def _get_cache_prefix(self, path: str) -> str:
|
336
336
|
"""Return a predictable unique key based on the given path."""
|
337
|
-
# DTFIX-RELEASE: choose a better hashing approach
|
338
337
|
return 'k' + hashlib.sha256(f'{self.ansible_name}{path}'.encode(), usedforsecurity=False).hexdigest()[:6]
|
339
338
|
|
340
339
|
def clear_cache(self) -> None:
|
@@ -402,6 +401,8 @@ class Constructable(_BaseInventoryPlugin):
|
|
402
401
|
|
403
402
|
def _add_host_to_keyed_groups(self, keys, variables, host, strict=False, fetch_hostvars=True):
|
404
403
|
""" helper to create groups for plugins based on variable values and add the corresponding hosts to it"""
|
404
|
+
should_default_value = (None, '')
|
405
|
+
|
405
406
|
if keys and isinstance(keys, list):
|
406
407
|
for keyed in keys:
|
407
408
|
if keyed and isinstance(keyed, dict):
|
@@ -418,7 +419,9 @@ class Constructable(_BaseInventoryPlugin):
|
|
418
419
|
trailing_separator = keyed.get('trailing_separator')
|
419
420
|
if trailing_separator is not None and default_value_name is not None:
|
420
421
|
raise AnsibleParserError("parameters are mutually exclusive for keyed groups: default_value|trailing_separator")
|
421
|
-
|
422
|
+
|
423
|
+
use_default = key in should_default_value and default_value_name is not None
|
424
|
+
if key or use_default:
|
422
425
|
prefix = keyed.get('prefix', '')
|
423
426
|
sep = keyed.get('separator', '_')
|
424
427
|
raw_parent_name = keyed.get('parent_group', None)
|
@@ -434,23 +437,21 @@ class Constructable(_BaseInventoryPlugin):
|
|
434
437
|
continue
|
435
438
|
|
436
439
|
new_raw_group_names = []
|
437
|
-
if
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
else:
|
442
|
-
new_raw_group_names.append(key)
|
440
|
+
if use_default:
|
441
|
+
new_raw_group_names.append(default_value_name)
|
442
|
+
elif isinstance(key, string_types):
|
443
|
+
new_raw_group_names.append(key)
|
443
444
|
elif isinstance(key, list):
|
444
445
|
for name in key:
|
445
446
|
# if list item is empty, 'default_value' will be used as group name
|
446
|
-
if name
|
447
|
+
if name in should_default_value and default_value_name is not None:
|
447
448
|
new_raw_group_names.append(default_value_name)
|
448
449
|
else:
|
449
450
|
new_raw_group_names.append(name)
|
450
451
|
elif isinstance(key, Mapping):
|
451
452
|
for (gname, gval) in key.items():
|
452
453
|
bare_name = '%s%s%s' % (gname, sep, gval)
|
453
|
-
if gval
|
454
|
+
if gval in should_default_value:
|
454
455
|
# key's value is empty
|
455
456
|
if default_value_name is not None:
|
456
457
|
bare_name = '%s%s%s' % (gname, sep, default_value_name)
|
@@ -367,7 +367,7 @@ def run_command(path: str, options: list[str], origin: Origin) -> tuple[str, str
|
|
367
367
|
if stderr and not stderr.endswith('\n'):
|
368
368
|
stderr += '\n'
|
369
369
|
|
370
|
-
# DTFIX-
|
370
|
+
# DTFIX-FUTURE: another use case for the "not quite help text, definitely not message" diagnostic output on errors
|
371
371
|
stderr_help_text = f'Standard error from inventory script:\n{stderr}' if stderr.strip() else None
|
372
372
|
|
373
373
|
if sp.returncode != 0:
|
ansible/plugins/list.py
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
6
|
|
7
|
+
import dataclasses
|
7
8
|
import os
|
8
9
|
|
9
10
|
from ansible import context
|
@@ -14,6 +15,7 @@ from ansible.module_utils.common.text.converters import to_native, to_bytes
|
|
14
15
|
from ansible.plugins import loader
|
15
16
|
from ansible.utils.display import Display
|
16
17
|
from ansible.utils.collection_loader._collection_finder import _get_collection_path
|
18
|
+
from ansible._internal._templating._jinja_plugins import get_jinja_builtin_plugin_descriptions
|
17
19
|
|
18
20
|
display = Display()
|
19
21
|
|
@@ -25,6 +27,20 @@ IGNORE = {
|
|
25
27
|
}
|
26
28
|
|
27
29
|
|
30
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
31
|
+
class _PluginDocMetadata:
|
32
|
+
"""Information about a plugin."""
|
33
|
+
|
34
|
+
name: str
|
35
|
+
"""The fully qualified name of the plugin."""
|
36
|
+
path: bytes | None = None
|
37
|
+
"""The path to the plugin file, or None if not available."""
|
38
|
+
plugin_obj: object | None = None
|
39
|
+
"""The loaded plugin object, or None if not loaded."""
|
40
|
+
jinja_builtin_short_description: str | None = None
|
41
|
+
"""The short description of the plugin if it is a Jinja builtin, otherwise None."""
|
42
|
+
|
43
|
+
|
28
44
|
def get_composite_name(collection, name, path, depth):
|
29
45
|
resolved_collection = collection
|
30
46
|
if '.' not in name:
|
@@ -116,21 +132,37 @@ def _list_j2_plugins_from_file(collection, plugin_path, ptype, plugin_name):
|
|
116
132
|
return file_plugins
|
117
133
|
|
118
134
|
|
119
|
-
def list_collection_plugins(ptype, collections, search_paths=None):
|
135
|
+
def list_collection_plugins(ptype: str, collections: dict[str, bytes], search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
|
136
|
+
# Kept for backwards compatibility.
|
137
|
+
return {
|
138
|
+
name: (info.path, info.plugin_obj)
|
139
|
+
for name, info in _list_collection_plugins_with_info(ptype, collections).items()
|
140
|
+
}
|
141
|
+
|
142
|
+
|
143
|
+
def _list_collection_plugins_with_info(
|
144
|
+
ptype: str,
|
145
|
+
collections: dict[str, bytes],
|
146
|
+
) -> dict[str, _PluginDocMetadata]:
|
120
147
|
# TODO: update to use importlib.resources
|
121
148
|
|
122
|
-
# starts at {plugin_name: filepath, ...}, but changes at the end
|
123
|
-
plugins = {}
|
124
149
|
try:
|
125
150
|
ploader = getattr(loader, '{0}_loader'.format(ptype))
|
126
151
|
except AttributeError:
|
127
152
|
raise AnsibleError(f"Cannot list plugins, incorrect plugin type {ptype!r} supplied.") from None
|
128
153
|
|
154
|
+
builtin_jinja_plugins = {}
|
155
|
+
plugin_paths = {}
|
156
|
+
|
129
157
|
# get plugins for each collection
|
130
|
-
for collection in collections.
|
158
|
+
for collection, path in collections.items():
|
131
159
|
if collection == 'ansible.builtin':
|
132
160
|
# dirs from ansible install, but not configured paths
|
133
161
|
dirs = [d.path for d in ploader._get_paths_with_context() if d.internal]
|
162
|
+
|
163
|
+
if ptype in ('filter', 'test'):
|
164
|
+
builtin_jinja_plugins = get_jinja_builtin_plugin_descriptions(ptype)
|
165
|
+
|
134
166
|
elif collection == 'ansible.legacy':
|
135
167
|
# configured paths + search paths (should include basedirs/-M)
|
136
168
|
dirs = [d.path for d in ploader._get_paths_with_context() if not d.internal]
|
@@ -139,7 +171,7 @@ def list_collection_plugins(ptype, collections, search_paths=None):
|
|
139
171
|
else:
|
140
172
|
# search path in this case is for locating collection itselfA
|
141
173
|
b_ptype = to_bytes(C.COLLECTION_PTYPE_COMPAT.get(ptype, ptype))
|
142
|
-
dirs = [to_native(os.path.join(
|
174
|
+
dirs = [to_native(os.path.join(path, b'plugins', b_ptype))]
|
143
175
|
# acr = AnsibleCollectionRef.try_parse_fqcr(collection, ptype)
|
144
176
|
# if acr:
|
145
177
|
# dirs = acr.subdirs
|
@@ -147,30 +179,51 @@ def list_collection_plugins(ptype, collections, search_paths=None):
|
|
147
179
|
|
148
180
|
# raise Exception('bad acr for %s, %s' % (collection, ptype))
|
149
181
|
|
150
|
-
|
182
|
+
plugin_paths.update(_list_plugins_from_paths(ptype, dirs, collection))
|
151
183
|
|
152
|
-
|
184
|
+
plugins = {}
|
153
185
|
if ptype in ('module',):
|
154
186
|
# no 'invalid' tests for modules
|
155
|
-
for plugin in
|
156
|
-
plugins[plugin] = (
|
187
|
+
for plugin, plugin_path in plugin_paths.items():
|
188
|
+
plugins[plugin] = _PluginDocMetadata(name=plugin, path=plugin_path)
|
157
189
|
else:
|
158
190
|
# detect invalid plugin candidates AND add loaded object to return data
|
159
|
-
for plugin in
|
191
|
+
for plugin, plugin_path in plugin_paths.items():
|
160
192
|
pobj = None
|
161
193
|
try:
|
162
194
|
pobj = ploader.get(plugin, class_only=True)
|
163
195
|
except Exception as e:
|
164
|
-
display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype,
|
196
|
+
display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype, plugin_path, to_native(e)))
|
165
197
|
|
166
|
-
|
167
|
-
|
198
|
+
plugins[plugin] = _PluginDocMetadata(
|
199
|
+
name=plugin,
|
200
|
+
path=plugin_path,
|
201
|
+
plugin_obj=pobj,
|
202
|
+
jinja_builtin_short_description=builtin_jinja_plugins.get(plugin),
|
203
|
+
)
|
204
|
+
|
205
|
+
# Add in any builtin Jinja2 plugins that have not been shadowed in Ansible.
|
206
|
+
plugins.update(
|
207
|
+
(plugin_name, _PluginDocMetadata(name=plugin_name, jinja_builtin_short_description=plugin_description))
|
208
|
+
for plugin_name, plugin_description in builtin_jinja_plugins.items() if plugin_name not in plugins
|
209
|
+
)
|
168
210
|
|
169
|
-
# {plugin_name: (filepath, class), ...}
|
170
211
|
return plugins
|
171
212
|
|
172
213
|
|
173
|
-
def list_plugins(ptype, collections=None, search_paths=None):
|
214
|
+
def list_plugins(ptype: str, collections: list[str] | None = None, search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
|
215
|
+
# Kept for backwards compatibility.
|
216
|
+
return {
|
217
|
+
name: (info.path, info.plugin_obj)
|
218
|
+
for name, info in _list_plugins_with_info(ptype, collections, search_paths).items()
|
219
|
+
}
|
220
|
+
|
221
|
+
|
222
|
+
def _list_plugins_with_info(
|
223
|
+
ptype: str,
|
224
|
+
collections: list[str] = None,
|
225
|
+
search_paths: list[str] | None = None,
|
226
|
+
) -> dict[str, _PluginDocMetadata]:
|
174
227
|
if isinstance(collections, str):
|
175
228
|
collections = [collections]
|
176
229
|
|
@@ -195,7 +248,7 @@ def list_plugins(ptype, collections=None, search_paths=None):
|
|
195
248
|
raise AnsibleError(f"Cannot use supplied collection {collection!r}.") from ex
|
196
249
|
|
197
250
|
if plugin_collections:
|
198
|
-
plugins.update(
|
251
|
+
plugins.update(_list_collection_plugins_with_info(ptype, plugin_collections))
|
199
252
|
|
200
253
|
return plugins
|
201
254
|
|
ansible/plugins/loader.py
CHANGED
@@ -26,6 +26,7 @@ from ansible import __version__ as ansible_version
|
|
26
26
|
from ansible import _internal, constants as C
|
27
27
|
from ansible.errors import AnsibleError, AnsiblePluginCircularRedirect, AnsiblePluginRemovedError, AnsibleCollectionUnsupportedVersionError
|
28
28
|
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
29
|
+
from ansible.module_utils.datatag import deprecator_from_collection_name
|
29
30
|
from ansible.module_utils.six import string_types
|
30
31
|
from ansible.parsing.yaml.loader import AnsibleLoader
|
31
32
|
from ansible._internal._yaml._loader import AnsibleInstrumentedLoader
|
@@ -40,7 +41,6 @@ from . import _AnsiblePluginInfoMixin
|
|
40
41
|
from .filter import AnsibleJinja2Filter
|
41
42
|
from .test import AnsibleJinja2Test
|
42
43
|
from .._internal._plugins import _cache
|
43
|
-
from ..module_utils.common.messages import PluginInfo
|
44
44
|
|
45
45
|
# TODO: take the packaging dep, or vendor SpecifierSet?
|
46
46
|
|
@@ -202,7 +202,7 @@ class PluginLoadContext(object):
|
|
202
202
|
msg=warning_text,
|
203
203
|
date=removal_date,
|
204
204
|
version=removal_version,
|
205
|
-
deprecator=
|
205
|
+
deprecator=deprecator_from_collection_name(collection_name),
|
206
206
|
)
|
207
207
|
|
208
208
|
self.deprecated = True
|
@@ -505,7 +505,8 @@ class PluginLoader:
|
|
505
505
|
|
506
506
|
# if type name != 'module_doc_fragment':
|
507
507
|
if type_name in C.CONFIGURABLE_PLUGINS and not C.config.has_configuration_definition(type_name, name):
|
508
|
-
|
508
|
+
# trust-tagged source propagates to loaded values; expressions and templates in config require trust
|
509
|
+
documentation_source = _tags.TrustedAsTemplate().tag(getattr(module, 'DOCUMENTATION', ''))
|
509
510
|
try:
|
510
511
|
dstring = yaml.load(_tags.Origin(path=path).tag(documentation_source), Loader=AnsibleLoader)
|
511
512
|
except ParserError as e:
|
@@ -610,7 +611,7 @@ class PluginLoader:
|
|
610
611
|
version=removal_version,
|
611
612
|
date=removal_date,
|
612
613
|
removed=True,
|
613
|
-
deprecator=
|
614
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
614
615
|
)
|
615
616
|
plugin_load_context.date = removal_date
|
616
617
|
plugin_load_context.version = removal_version
|
@@ -673,7 +674,7 @@ class PluginLoader:
|
|
673
674
|
# look for any matching extension in the package location (sans filter)
|
674
675
|
found_files = [f
|
675
676
|
for f in glob.iglob(os.path.join(pkg_path, n_resource) + '.*')
|
676
|
-
if os.path.isfile(f) and not f.endswith(C.MODULE_IGNORE_EXTS)]
|
677
|
+
if os.path.isfile(f) and not any(f.endswith(ext) for ext in C.MODULE_IGNORE_EXTS)]
|
677
678
|
|
678
679
|
if not found_files:
|
679
680
|
return plugin_load_context.nope('failed fuzzy extension match for {0} in {1}'.format(full_name, acr.collection))
|
@@ -794,7 +795,7 @@ class PluginLoader:
|
|
794
795
|
except Exception as ex:
|
795
796
|
plugin_load_context.raw_error_list.append(ex)
|
796
797
|
|
797
|
-
# DTFIX-
|
798
|
+
# DTFIX-FUTURE: can we deprecate/remove these stringified versions?
|
798
799
|
if isinstance(ex, ImportError):
|
799
800
|
plugin_load_context.import_error_list.append(ex)
|
800
801
|
else:
|
@@ -954,7 +955,7 @@ class PluginLoader:
|
|
954
955
|
redirected_names: list[str] | None = None,
|
955
956
|
resolved: str | None = None,
|
956
957
|
) -> None:
|
957
|
-
# DTFIX-
|
958
|
+
# DTFIX-FUTURE: clean this up- standardize types, document, split/remove redundant bits
|
958
959
|
|
959
960
|
# set extra info on the module, in case we want it later
|
960
961
|
obj._original_path = path
|
@@ -1395,7 +1396,7 @@ class Jinja2Loader(PluginLoader):
|
|
1395
1396
|
msg=warning_text,
|
1396
1397
|
version=removal_version,
|
1397
1398
|
date=removal_date,
|
1398
|
-
deprecator=
|
1399
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
1399
1400
|
)
|
1400
1401
|
|
1401
1402
|
# check removal
|
@@ -1411,7 +1412,7 @@ class Jinja2Loader(PluginLoader):
|
|
1411
1412
|
version=removal_version,
|
1412
1413
|
date=removal_date,
|
1413
1414
|
removed=True,
|
1414
|
-
deprecator=
|
1415
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
1415
1416
|
)
|
1416
1417
|
|
1417
1418
|
raise AnsiblePluginRemovedError(exc_msg)
|