ansible-core 2.19.3rc1__py3-none-any.whl → 2.20.0b2__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.
Potentially problematic release.
This version of ansible-core might be problematic. Click here for more details.
- ansible/_internal/__init__.py +1 -4
- ansible/_internal/_ansiballz/_builder.py +1 -3
- ansible/_internal/_collection_proxy.py +7 -9
- ansible/_internal/_json/__init__.py +3 -4
- ansible/_internal/_templating/_engine.py +1 -1
- ansible/_internal/_templating/_jinja_plugins.py +1 -2
- ansible/_internal/_wrapt.py +105 -301
- ansible/cli/__init__.py +11 -10
- ansible/cli/adhoc.py +1 -2
- ansible/cli/arguments/option_helpers.py +1 -1
- ansible/cli/config.py +5 -6
- ansible/cli/doc.py +70 -68
- ansible/cli/galaxy.py +15 -24
- ansible/cli/inventory.py +0 -1
- ansible/cli/playbook.py +0 -1
- ansible/cli/pull.py +0 -1
- ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
- ansible/collections/list.py +4 -2
- ansible/config/base.yml +1 -25
- ansible/config/manager.py +0 -2
- ansible/executor/play_iterator.py +42 -20
- ansible/executor/playbook_executor.py +0 -9
- ansible/executor/task_executor.py +26 -18
- ansible/executor/task_queue_manager.py +1 -3
- ansible/galaxy/api.py +33 -80
- ansible/galaxy/collection/__init__.py +4 -17
- ansible/galaxy/dependency_resolution/dataclasses.py +0 -10
- ansible/galaxy/dependency_resolution/providers.py +24 -118
- ansible/galaxy/role.py +1 -33
- ansible/inventory/manager.py +2 -3
- ansible/keyword_desc.yml +0 -3
- ansible/module_utils/_internal/_datatag/__init__.py +2 -10
- ansible/module_utils/_internal/_no_six.py +86 -0
- ansible/module_utils/_text.py +28 -8
- ansible/module_utils/ansible_release.py +2 -2
- ansible/module_utils/basic.py +26 -23
- ansible/module_utils/common/_collections_compat.py +11 -2
- ansible/module_utils/common/collections.py +8 -3
- ansible/module_utils/common/dict_transformations.py +1 -2
- ansible/module_utils/common/network.py +4 -2
- ansible/module_utils/common/parameters.py +32 -41
- ansible/module_utils/common/text/converters.py +109 -23
- ansible/module_utils/common/text/formatters.py +6 -2
- ansible/module_utils/common/validation.py +11 -9
- ansible/module_utils/connection.py +8 -3
- ansible/module_utils/facts/hardware/linux.py +23 -7
- ansible/module_utils/facts/hardware/netbsd.py +1 -1
- ansible/module_utils/facts/hardware/sunos.py +2 -1
- ansible/module_utils/facts/packages.py +6 -2
- ansible/module_utils/facts/system/distribution.py +2 -1
- ansible/module_utils/facts/system/env.py +6 -3
- ansible/module_utils/facts/system/local.py +3 -1
- ansible/module_utils/parsing/convert_bool.py +6 -2
- ansible/module_utils/service.py +2 -3
- ansible/module_utils/six/__init__.py +19 -6
- ansible/module_utils/yumdnf.py +0 -5
- ansible/modules/apt.py +18 -13
- ansible/modules/apt_repository.py +1 -1
- ansible/modules/assemble.py +5 -9
- ansible/modules/blockinfile.py +39 -23
- ansible/modules/cron.py +26 -35
- ansible/modules/deb822_repository.py +83 -12
- ansible/modules/dnf.py +3 -7
- ansible/modules/dnf5.py +4 -6
- ansible/modules/expect.py +0 -3
- ansible/modules/find.py +1 -2
- ansible/modules/get_url.py +1 -1
- ansible/modules/git.py +4 -5
- ansible/modules/include_vars.py +1 -1
- ansible/modules/known_hosts.py +7 -1
- ansible/modules/lineinfile.py +71 -63
- ansible/modules/package_facts.py +1 -1
- ansible/modules/pip.py +8 -2
- ansible/modules/replace.py +6 -6
- ansible/modules/service.py +3 -4
- ansible/modules/stat.py +20 -0
- ansible/modules/uri.py +9 -10
- ansible/modules/user.py +1 -2
- ansible/modules/wait_for.py +2 -2
- ansible/modules/wait_for_connection.py +2 -1
- ansible/modules/yum_repository.py +1 -16
- ansible/parsing/dataloader.py +24 -31
- ansible/parsing/mod_args.py +3 -0
- ansible/parsing/vault/__init__.py +1 -2
- ansible/playbook/base.py +8 -56
- ansible/playbook/block.py +1 -63
- ansible/playbook/collectionsearch.py +1 -2
- ansible/playbook/handler.py +1 -7
- ansible/playbook/helpers.py +15 -20
- ansible/playbook/included_file.py +1 -1
- ansible/playbook/play.py +105 -49
- ansible/playbook/play_context.py +4 -0
- ansible/playbook/role/__init__.py +10 -65
- ansible/playbook/role/definition.py +3 -4
- ansible/playbook/role/include.py +2 -3
- ansible/playbook/role/metadata.py +1 -12
- ansible/playbook/role/requirement.py +1 -2
- ansible/playbook/role_include.py +1 -2
- ansible/playbook/taggable.py +16 -5
- ansible/playbook/task.py +51 -55
- ansible/plugins/action/__init__.py +20 -19
- ansible/plugins/action/add_host.py +1 -2
- ansible/plugins/action/fetch.py +3 -5
- ansible/plugins/action/group_by.py +1 -2
- ansible/plugins/action/include_vars.py +20 -22
- ansible/plugins/action/script.py +1 -3
- ansible/plugins/action/template.py +1 -2
- ansible/plugins/action/uri.py +4 -2
- ansible/plugins/cache/__init__.py +1 -0
- ansible/plugins/callback/__init__.py +13 -6
- ansible/plugins/connection/__init__.py +3 -7
- ansible/plugins/connection/local.py +2 -3
- ansible/plugins/connection/psrp.py +0 -2
- ansible/plugins/connection/ssh.py +2 -7
- ansible/plugins/connection/winrm.py +0 -2
- ansible/plugins/doc_fragments/result_format_callback.py +15 -0
- ansible/plugins/filter/core.py +4 -5
- ansible/plugins/filter/encryption.py +3 -27
- ansible/plugins/filter/mathstuff.py +1 -2
- ansible/plugins/filter/to_nice_yaml.yml +31 -3
- ansible/plugins/filter/to_yaml.yml +29 -12
- ansible/plugins/inventory/__init__.py +1 -2
- ansible/plugins/inventory/toml.py +3 -6
- ansible/plugins/inventory/yaml.py +1 -2
- ansible/plugins/loader.py +3 -4
- ansible/plugins/lookup/password.py +1 -2
- ansible/plugins/lookup/subelements.py +2 -3
- ansible/plugins/lookup/url.py +1 -1
- ansible/plugins/lookup/varnames.py +1 -2
- ansible/plugins/shell/__init__.py +9 -4
- ansible/plugins/shell/powershell.py +8 -24
- ansible/plugins/strategy/__init__.py +6 -3
- ansible/plugins/test/core.py +4 -1
- ansible/plugins/test/falsy.yml +1 -1
- ansible/plugins/test/regex.yml +18 -6
- ansible/plugins/test/truthy.yml +1 -1
- ansible/release.py +2 -2
- ansible/template/__init__.py +3 -7
- ansible/utils/collection_loader/_collection_config.py +5 -0
- ansible/utils/collection_loader/_collection_finder.py +11 -14
- ansible/utils/context_objects.py +7 -4
- ansible/utils/display.py +7 -6
- ansible/utils/encrypt.py +0 -5
- ansible/utils/helpers.py +6 -2
- ansible/utils/jsonrpc.py +7 -3
- ansible/utils/plugin_docs.py +49 -38
- ansible/utils/ssh_functions.py +0 -19
- ansible/utils/unsafe_proxy.py +7 -7
- ansible/vars/clean.py +2 -3
- ansible/vars/manager.py +28 -22
- ansible/vars/plugins.py +1 -31
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/METADATA +3 -3
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/RECORD +199 -200
- ansible_test/_data/completion/docker.txt +7 -7
- ansible_test/_data/completion/network.txt +0 -1
- ansible_test/_data/completion/remote.txt +4 -4
- ansible_test/_data/requirements/ansible-test.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +1 -1
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_internal/cache.py +2 -5
- ansible_test/_internal/cli/compat.py +1 -1
- ansible_test/_internal/commands/coverage/combine.py +1 -3
- ansible_test/_internal/commands/integration/__init__.py +3 -7
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/integration/coverage.py +1 -3
- ansible_test/_internal/commands/integration/filters.py +5 -10
- ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
- ansible_test/_internal/commands/units/__init__.py +1 -13
- ansible_test/_internal/completion.py +2 -5
- ansible_test/_internal/config.py +2 -7
- ansible_test/_internal/coverage_util.py +1 -1
- ansible_test/_internal/delegation.py +2 -0
- ansible_test/_internal/docker_util.py +1 -1
- ansible_test/_internal/host_profiles.py +6 -11
- ansible_test/_internal/provider/__init__.py +2 -5
- ansible_test/_internal/provisioning.py +2 -5
- ansible_test/_internal/pypi_proxy.py +1 -1
- ansible_test/_internal/target.py +2 -6
- ansible_test/_internal/thread.py +1 -4
- ansible_test/_internal/util.py +9 -14
- ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
- ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +40 -27
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
- ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
- ansible_test/_util/target/common/constants.py +2 -2
- ansible_test/_util/target/setup/bootstrap.sh +0 -6
- ansible/utils/py3compat.py +0 -27
- ansible_test/_data/pytest/config/legacy.ini +0 -4
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/top_level.txt +0 -0
ansible/playbook/taggable.py
CHANGED
|
@@ -19,11 +19,14 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import typing as t
|
|
21
21
|
|
|
22
|
+
from ansible._internal._templating._engine import TemplateEngine
|
|
22
23
|
from ansible.errors import AnsibleError
|
|
23
24
|
from ansible.module_utils.common.sentinel import Sentinel
|
|
24
25
|
from ansible.module_utils._internal._datatag import AnsibleTagHelper
|
|
25
26
|
from ansible.playbook.attribute import FieldAttribute
|
|
26
|
-
from ansible.
|
|
27
|
+
from ansible.utils.display import Display
|
|
28
|
+
|
|
29
|
+
_display = Display()
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def _flatten_tags(tags: list[str | int]) -> list[str | int]:
|
|
@@ -38,17 +41,25 @@ def _flatten_tags(tags: list[str | int]) -> list[str | int]:
|
|
|
38
41
|
|
|
39
42
|
class Taggable:
|
|
40
43
|
|
|
44
|
+
_RESERVED = frozenset(['tagged', 'all', 'untagged'])
|
|
41
45
|
untagged = frozenset(['untagged'])
|
|
42
46
|
tags = FieldAttribute(isa='list', default=list, listof=(str, int), extend=True)
|
|
43
47
|
|
|
44
48
|
def _load_tags(self, attr, ds):
|
|
49
|
+
|
|
50
|
+
tags = None
|
|
45
51
|
if isinstance(ds, list):
|
|
46
|
-
|
|
52
|
+
tags = ds
|
|
53
|
+
elif isinstance(ds, str):
|
|
54
|
+
tags = [AnsibleTagHelper.tag_copy(ds, item.strip()) for item in ds.split(',')]
|
|
55
|
+
|
|
56
|
+
if tags is None:
|
|
57
|
+
raise AnsibleError('tags must be specified as a list', obj=ds)
|
|
47
58
|
|
|
48
|
-
if
|
|
49
|
-
|
|
59
|
+
if found := self._RESERVED.intersection(tags):
|
|
60
|
+
_display.warning(f"Found reserved tagnames in tags: {list(found)!r}, we do not recommend doing this as it might give unexpected results", obj=ds)
|
|
50
61
|
|
|
51
|
-
|
|
62
|
+
return tags
|
|
52
63
|
|
|
53
64
|
def _get_all_taggable_objects(self) -> t.Iterable[Taggable]:
|
|
54
65
|
obj = self
|
ansible/playbook/task.py
CHANGED
|
@@ -25,7 +25,6 @@ from ansible.errors import AnsibleError, AnsibleParserError, AnsibleUndefinedVar
|
|
|
25
25
|
from ansible.executor.module_common import _get_action_arg_defaults
|
|
26
26
|
from ansible.module_utils.common.text.converters import to_native
|
|
27
27
|
from ansible.module_utils._internal._datatag import AnsibleTagHelper
|
|
28
|
-
from ansible.module_utils.six import string_types
|
|
29
28
|
from ansible.parsing.mod_args import ModuleArgsParser, RAW_PARAM_MODULES
|
|
30
29
|
from ansible.plugins.action import ActionBase
|
|
31
30
|
from ansible.plugins.loader import action_loader, module_loader, lookup_loader
|
|
@@ -37,11 +36,10 @@ from ansible.playbook.conditional import Conditional
|
|
|
37
36
|
from ansible.playbook.delegatable import Delegatable
|
|
38
37
|
from ansible.playbook.loop_control import LoopControl
|
|
39
38
|
from ansible.playbook.notifiable import Notifiable
|
|
40
|
-
from ansible.playbook.role import Role
|
|
41
39
|
from ansible.playbook.taggable import Taggable
|
|
42
40
|
from ansible._internal import _task
|
|
43
41
|
from ansible._internal._templating import _marker_behaviors
|
|
44
|
-
from ansible._internal._templating._jinja_bits import is_possibly_all_template
|
|
42
|
+
from ansible._internal._templating._jinja_bits import is_possibly_all_template, is_possibly_template
|
|
45
43
|
from ansible._internal._templating._engine import TemplateEngine, TemplateOptions
|
|
46
44
|
from ansible.utils.collection_loader import AnsibleCollectionConfig
|
|
47
45
|
from ansible.utils.display import Display
|
|
@@ -101,7 +99,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
101
99
|
self._role = role
|
|
102
100
|
self._parent = None
|
|
103
101
|
self.implicit = False
|
|
104
|
-
self.
|
|
102
|
+
self._resolved_action: str | None = None
|
|
105
103
|
|
|
106
104
|
if task_include:
|
|
107
105
|
self._parent = task_include
|
|
@@ -110,6 +108,38 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
110
108
|
|
|
111
109
|
super(Task, self).__init__()
|
|
112
110
|
|
|
111
|
+
_resolved_action_warning = (
|
|
112
|
+
"A plugin is sampling the task's resolved_action when it is not resolved. "
|
|
113
|
+
"This can be caused by callback plugins using the resolved_action attribute too "
|
|
114
|
+
"early (such as in v2_playbook_on_task_start for a task using the action/local_action "
|
|
115
|
+
"keyword), or too late (such as in v2_runner_on_ok for a task with a loop). "
|
|
116
|
+
"To maximize compatibility with user features, callback plugins should "
|
|
117
|
+
"only use this attribute in v2_runner_on_ok/v2_runner_on_failed for tasks "
|
|
118
|
+
"without a loop, and v2_runner_item_on_ok/v2_runner_item_on_failed otherwise."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def resolved_action(self) -> str | None:
|
|
123
|
+
"""The templated and resolved FQCN of the task action or None.
|
|
124
|
+
|
|
125
|
+
If the action is a template, callback plugins can only use this value in certain methods.
|
|
126
|
+
- v2_runner_on_ok and v2_runner_on_failed if there's no task loop
|
|
127
|
+
- v2_runner_item_on_ok and v2_runner_item_on_failed if there is a task loop
|
|
128
|
+
"""
|
|
129
|
+
# Consider deprecating this because it's difficult to use?
|
|
130
|
+
# Moving it to the task result would improve the no-loop limitation on v2_runner_on_ok
|
|
131
|
+
# but then wouldn't be accessible to v2_playbook_on_task_start, *_on_skipped, etc.
|
|
132
|
+
if self._resolved_action is not None:
|
|
133
|
+
return self._resolved_action
|
|
134
|
+
if not is_possibly_template(self.action):
|
|
135
|
+
try:
|
|
136
|
+
return self._resolve_action(self.action)
|
|
137
|
+
except AnsibleParserError:
|
|
138
|
+
display.warning(self._resolved_action_warning, obj=self.action)
|
|
139
|
+
else:
|
|
140
|
+
display.warning(self._resolved_action_warning, obj=self.action)
|
|
141
|
+
return None
|
|
142
|
+
|
|
113
143
|
def get_name(self, include_role_fqcn=True):
|
|
114
144
|
""" return the name of the task """
|
|
115
145
|
|
|
@@ -129,7 +159,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
129
159
|
def _merge_kv(self, ds):
|
|
130
160
|
if ds is None:
|
|
131
161
|
return ""
|
|
132
|
-
elif isinstance(ds,
|
|
162
|
+
elif isinstance(ds, str):
|
|
133
163
|
return ds
|
|
134
164
|
elif isinstance(ds, dict):
|
|
135
165
|
buf = ""
|
|
@@ -168,7 +198,7 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
168
198
|
else:
|
|
169
199
|
module_or_action_context = action_context.plugin_load_context
|
|
170
200
|
|
|
171
|
-
self.
|
|
201
|
+
self._resolved_action = module_or_action_context.resolved_fqcn
|
|
172
202
|
|
|
173
203
|
action_type: type[ActionBase] = action_context.object
|
|
174
204
|
|
|
@@ -282,6 +312,9 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
282
312
|
# But if it wasn't, we can add the yaml object now to get more detail
|
|
283
313
|
raise AnsibleParserError("Error parsing task arguments.", obj=ds) from ex
|
|
284
314
|
|
|
315
|
+
if args_parser._resolved_action is not None:
|
|
316
|
+
self._resolved_action = args_parser._resolved_action
|
|
317
|
+
|
|
285
318
|
new_ds['action'] = action
|
|
286
319
|
new_ds['args'] = args
|
|
287
320
|
new_ds['delegate_to'] = delegate_to
|
|
@@ -465,58 +498,11 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
465
498
|
new_me._role = self._role
|
|
466
499
|
|
|
467
500
|
new_me.implicit = self.implicit
|
|
468
|
-
new_me.
|
|
501
|
+
new_me._resolved_action = self._resolved_action
|
|
469
502
|
new_me._uuid = self._uuid
|
|
470
503
|
|
|
471
504
|
return new_me
|
|
472
505
|
|
|
473
|
-
def serialize(self):
|
|
474
|
-
data = super(Task, self).serialize()
|
|
475
|
-
|
|
476
|
-
if not self._squashed and not self._finalized:
|
|
477
|
-
if self._parent:
|
|
478
|
-
data['parent'] = self._parent.serialize()
|
|
479
|
-
data['parent_type'] = self._parent.__class__.__name__
|
|
480
|
-
|
|
481
|
-
if self._role:
|
|
482
|
-
data['role'] = self._role.serialize()
|
|
483
|
-
|
|
484
|
-
data['implicit'] = self.implicit
|
|
485
|
-
data['resolved_action'] = self.resolved_action
|
|
486
|
-
|
|
487
|
-
return data
|
|
488
|
-
|
|
489
|
-
def deserialize(self, data):
|
|
490
|
-
|
|
491
|
-
# import is here to avoid import loops
|
|
492
|
-
from ansible.playbook.task_include import TaskInclude
|
|
493
|
-
from ansible.playbook.handler_task_include import HandlerTaskInclude
|
|
494
|
-
|
|
495
|
-
parent_data = data.get('parent', None)
|
|
496
|
-
if parent_data:
|
|
497
|
-
parent_type = data.get('parent_type')
|
|
498
|
-
if parent_type == 'Block':
|
|
499
|
-
p = Block()
|
|
500
|
-
elif parent_type == 'TaskInclude':
|
|
501
|
-
p = TaskInclude()
|
|
502
|
-
elif parent_type == 'HandlerTaskInclude':
|
|
503
|
-
p = HandlerTaskInclude()
|
|
504
|
-
p.deserialize(parent_data)
|
|
505
|
-
self._parent = p
|
|
506
|
-
del data['parent']
|
|
507
|
-
|
|
508
|
-
role_data = data.get('role')
|
|
509
|
-
if role_data:
|
|
510
|
-
r = Role()
|
|
511
|
-
r.deserialize(role_data)
|
|
512
|
-
self._role = r
|
|
513
|
-
del data['role']
|
|
514
|
-
|
|
515
|
-
self.implicit = data.get('implicit', False)
|
|
516
|
-
self.resolved_action = data.get('resolved_action')
|
|
517
|
-
|
|
518
|
-
super(Task, self).deserialize(data)
|
|
519
|
-
|
|
520
506
|
def set_loader(self, loader):
|
|
521
507
|
"""
|
|
522
508
|
Sets the loader on this object and recursively on parent, child objects.
|
|
@@ -591,9 +577,19 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
|
|
|
591
577
|
def dump_attrs(self):
|
|
592
578
|
"""Override to smuggle important non-FieldAttribute values back to the controller."""
|
|
593
579
|
attrs = super().dump_attrs()
|
|
594
|
-
attrs.update(
|
|
580
|
+
attrs.update(_resolved_action=self._resolved_action)
|
|
595
581
|
return attrs
|
|
596
582
|
|
|
583
|
+
def from_attrs(self, attrs):
|
|
584
|
+
super().from_attrs(attrs)
|
|
585
|
+
|
|
586
|
+
# from_attrs is only used to create a finalized task
|
|
587
|
+
# from attrs from the Worker/TaskExecutor
|
|
588
|
+
# Those attrs are finalized and squashed in the TE
|
|
589
|
+
# and controller side use needs to reflect that
|
|
590
|
+
self._finalized = True
|
|
591
|
+
self._squashed = True
|
|
592
|
+
|
|
597
593
|
def _resolve_conditional(
|
|
598
594
|
self,
|
|
599
595
|
conditional: list[str | bool],
|
|
@@ -29,7 +29,6 @@ from ansible.module_utils.common.arg_spec import ArgumentSpecValidator
|
|
|
29
29
|
from ansible.module_utils.errors import UnsupportedError
|
|
30
30
|
from ansible.module_utils.json_utils import _filter_non_json_lines
|
|
31
31
|
from ansible.module_utils.common.json import Direction, get_module_encoder, get_module_decoder
|
|
32
|
-
from ansible.module_utils.six import binary_type, string_types, text_type
|
|
33
32
|
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
|
34
33
|
from ansible.release import __version__
|
|
35
34
|
from ansible.utils.collection_loader import resource_from_fqcr
|
|
@@ -52,7 +51,7 @@ if t.TYPE_CHECKING:
|
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
def _validate_utf8_json(d):
|
|
55
|
-
if isinstance(d,
|
|
54
|
+
if isinstance(d, str):
|
|
56
55
|
# Purposefully not using to_bytes here for performance reasons
|
|
57
56
|
d.encode(encoding='utf-8', errors='strict')
|
|
58
57
|
elif isinstance(d, dict):
|
|
@@ -288,14 +287,6 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
288
287
|
elif leaf_module_name == 'async_status' and collection_name in rewrite_collection_names:
|
|
289
288
|
module_name = '%s.%s' % (win_collection, leaf_module_name)
|
|
290
289
|
|
|
291
|
-
# TODO: move this tweak down to the modules, not extensible here
|
|
292
|
-
# Remove extra quotes surrounding path parameters before sending to module.
|
|
293
|
-
if leaf_module_name in ['win_stat', 'win_file', 'win_copy', 'slurp'] and module_args and \
|
|
294
|
-
hasattr(self._connection._shell, '_unquote'):
|
|
295
|
-
for key in ('src', 'dest', 'path'):
|
|
296
|
-
if key in module_args:
|
|
297
|
-
module_args[key] = self._connection._shell._unquote(module_args[key])
|
|
298
|
-
|
|
299
290
|
result = self._shared_loader_obj.module_loader.find_plugin_with_context(module_name, mod_type, collection_list=self._task.collections)
|
|
300
291
|
|
|
301
292
|
if not result.resolved:
|
|
@@ -680,8 +671,18 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
680
671
|
become_user,
|
|
681
672
|
setfacl_mode)
|
|
682
673
|
|
|
683
|
-
|
|
684
|
-
|
|
674
|
+
match res.get('rc'):
|
|
675
|
+
case 0:
|
|
676
|
+
return remote_paths
|
|
677
|
+
case 2:
|
|
678
|
+
# invalid syntax (for example, missing user, missing colon)
|
|
679
|
+
self._display.debug(f"setfacl command failed with an invalid syntax. Trying chmod instead. Err: {res!r}")
|
|
680
|
+
case 127:
|
|
681
|
+
# setfacl binary does not exists or we don't have permission to use it.
|
|
682
|
+
self._display.debug(f"setfacl binary does not exist or does not have permission to use it. Trying chmod instead. Err: {res!r}")
|
|
683
|
+
case _:
|
|
684
|
+
# generic debug message
|
|
685
|
+
self._display.debug(f'Failed to set facl {setfacl_mode}, got:{res!r}')
|
|
685
686
|
|
|
686
687
|
# Step 3b: Set execute if we need to. We do this before anything else
|
|
687
688
|
# because some of the methods below might work but not let us set
|
|
@@ -874,7 +875,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
874
875
|
# happens sometimes when it is a dir and not on bsd
|
|
875
876
|
if 'checksum' not in mystat['stat']:
|
|
876
877
|
mystat['stat']['checksum'] = ''
|
|
877
|
-
elif not isinstance(mystat['stat']['checksum'],
|
|
878
|
+
elif not isinstance(mystat['stat']['checksum'], str):
|
|
878
879
|
raise AnsibleError("Invalid checksum returned by stat: expected a string type but got %s" % type(mystat['stat']['checksum']))
|
|
879
880
|
|
|
880
881
|
return mystat['stat']
|
|
@@ -1084,7 +1085,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
1084
1085
|
# the remote system, which can be read and parsed by the module
|
|
1085
1086
|
args_data = ""
|
|
1086
1087
|
for k, v in module_args.items():
|
|
1087
|
-
args_data += '%s=%s ' % (k, shlex.quote(
|
|
1088
|
+
args_data += '%s=%s ' % (k, shlex.quote(str(v)))
|
|
1088
1089
|
self._transfer_data(args_file_path, args_data)
|
|
1089
1090
|
elif module_style in ('non_native_want_json', 'binary'):
|
|
1090
1091
|
profile_encoder = get_module_encoder(module_bits.serialization_profile, Direction.CONTROLLER_TO_MODULE)
|
|
@@ -1169,7 +1170,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
1169
1170
|
self._cleanup_remote_tmp = False
|
|
1170
1171
|
|
|
1171
1172
|
# NOTE: dnf returns results .. but that made it 'compatible' with squashing, so we allow mappings, for now
|
|
1172
|
-
if 'results' in data and (not isinstance(data['results'], Sequence) or isinstance(data['results'],
|
|
1173
|
+
if 'results' in data and (not isinstance(data['results'], Sequence) or isinstance(data['results'], str)):
|
|
1173
1174
|
data['ansible_module_results'] = data['results']
|
|
1174
1175
|
del data['results']
|
|
1175
1176
|
display.warning("Found internal 'results' key in module return, renamed to 'ansible_module_results'.")
|
|
@@ -1322,16 +1323,16 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
|
|
|
1322
1323
|
|
|
1323
1324
|
# stdout and stderr may be either a file-like or a bytes object.
|
|
1324
1325
|
# Convert either one to a text type
|
|
1325
|
-
if isinstance(stdout,
|
|
1326
|
+
if isinstance(stdout, bytes):
|
|
1326
1327
|
out = to_text(stdout, errors=encoding_errors)
|
|
1327
|
-
elif not isinstance(stdout,
|
|
1328
|
+
elif not isinstance(stdout, str):
|
|
1328
1329
|
out = to_text(b''.join(stdout.readlines()), errors=encoding_errors)
|
|
1329
1330
|
else:
|
|
1330
1331
|
out = stdout
|
|
1331
1332
|
|
|
1332
|
-
if isinstance(stderr,
|
|
1333
|
+
if isinstance(stderr, bytes):
|
|
1333
1334
|
err = to_text(stderr, errors=encoding_errors)
|
|
1334
|
-
elif not isinstance(stderr,
|
|
1335
|
+
elif not isinstance(stderr, str):
|
|
1335
1336
|
err = to_text(b''.join(stderr.readlines()), errors=encoding_errors)
|
|
1336
1337
|
else:
|
|
1337
1338
|
err = stderr
|
|
@@ -21,7 +21,6 @@ from __future__ import annotations
|
|
|
21
21
|
from collections.abc import Mapping
|
|
22
22
|
|
|
23
23
|
from ansible.errors import AnsibleActionFail
|
|
24
|
-
from ansible.module_utils.six import string_types
|
|
25
24
|
from ansible.plugins.action import ActionBase
|
|
26
25
|
from ansible.parsing.utils.addresses import parse_address
|
|
27
26
|
from ansible.utils.display import Display
|
|
@@ -74,7 +73,7 @@ class ActionModule(ActionBase):
|
|
|
74
73
|
if groups:
|
|
75
74
|
if isinstance(groups, list):
|
|
76
75
|
group_list = groups
|
|
77
|
-
elif isinstance(groups,
|
|
76
|
+
elif isinstance(groups, str):
|
|
78
77
|
group_list = groups.split(",")
|
|
79
78
|
else:
|
|
80
79
|
raise AnsibleActionFail("Groups must be specified as a list.", obj=groups)
|
ansible/plugins/action/fetch.py
CHANGED
|
@@ -20,7 +20,6 @@ import os
|
|
|
20
20
|
import base64
|
|
21
21
|
from ansible.errors import AnsibleConnectionFailure, AnsibleError, AnsibleActionFail, AnsibleActionSkip
|
|
22
22
|
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
|
23
|
-
from ansible.module_utils.six import string_types
|
|
24
23
|
from ansible.module_utils.parsing.convert_bool import boolean
|
|
25
24
|
from ansible.plugins.action import ActionBase
|
|
26
25
|
from ansible.utils.display import Display
|
|
@@ -52,10 +51,10 @@ class ActionModule(ActionBase):
|
|
|
52
51
|
|
|
53
52
|
msg = ''
|
|
54
53
|
# FIXME: validate source and dest are strings; use basic.py and module specs
|
|
55
|
-
if not isinstance(source,
|
|
54
|
+
if not isinstance(source, str):
|
|
56
55
|
msg = "Invalid type supplied for source option, it must be a string"
|
|
57
56
|
|
|
58
|
-
if not isinstance(dest,
|
|
57
|
+
if not isinstance(dest, str):
|
|
59
58
|
msg = "Invalid type supplied for dest option, it must be a string"
|
|
60
59
|
|
|
61
60
|
if source is None or dest is None:
|
|
@@ -131,7 +130,6 @@ class ActionModule(ActionBase):
|
|
|
131
130
|
|
|
132
131
|
# calculate the destination name
|
|
133
132
|
if os.path.sep not in self._connection._shell.join_path('a', ''):
|
|
134
|
-
source = self._connection._shell._unquote(source)
|
|
135
133
|
source_local = source.replace('\\', '/')
|
|
136
134
|
else:
|
|
137
135
|
source_local = source
|
|
@@ -194,7 +192,7 @@ class ActionModule(ActionBase):
|
|
|
194
192
|
msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None,
|
|
195
193
|
checksum=new_checksum, remote_checksum=remote_checksum))
|
|
196
194
|
else:
|
|
197
|
-
result.update({'changed': True, 'md5sum': new_md5, 'dest': dest,
|
|
195
|
+
result.update({'changed': True, 'md5sum': new_md5, 'file': source, 'dest': dest,
|
|
198
196
|
'remote_md5sum': None, 'checksum': new_checksum,
|
|
199
197
|
'remote_checksum': remote_checksum})
|
|
200
198
|
else:
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from ansible.plugins.action import ActionBase
|
|
20
|
-
from ansible.module_utils.six import string_types
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class ActionModule(ActionBase):
|
|
@@ -42,7 +41,7 @@ class ActionModule(ActionBase):
|
|
|
42
41
|
|
|
43
42
|
group_name = self._task.args.get('key')
|
|
44
43
|
parent_groups = self._task.args.get('parents', ['all'])
|
|
45
|
-
if isinstance(parent_groups,
|
|
44
|
+
if isinstance(parent_groups, str):
|
|
46
45
|
parent_groups = [parent_groups]
|
|
47
46
|
|
|
48
47
|
result['changed'] = False
|
|
@@ -10,7 +10,6 @@ import pathlib
|
|
|
10
10
|
import ansible.constants as C
|
|
11
11
|
from ansible.errors import AnsibleError
|
|
12
12
|
from ansible._internal._datatag._tags import SourceWasEncrypted
|
|
13
|
-
from ansible.module_utils.six import string_types
|
|
14
13
|
from ansible.module_utils.common.text.converters import to_native
|
|
15
14
|
from ansible.plugins.action import ActionBase
|
|
16
15
|
from ansible.utils.vars import combine_vars
|
|
@@ -38,14 +37,17 @@ class ActionModule(ActionBase):
|
|
|
38
37
|
if not self.ignore_files:
|
|
39
38
|
self.ignore_files = list()
|
|
40
39
|
|
|
41
|
-
if isinstance(self.ignore_files,
|
|
40
|
+
if isinstance(self.ignore_files, str):
|
|
41
|
+
self._display.deprecated(
|
|
42
|
+
msg="Specifying 'ignore_files' as a string is deprecated.",
|
|
43
|
+
version="2.24",
|
|
44
|
+
help_text="Use a list of strings instead.",
|
|
45
|
+
obj=self.ignore_files,
|
|
46
|
+
)
|
|
42
47
|
self.ignore_files = self.ignore_files.split()
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'failed': True,
|
|
47
|
-
'message': '{0} must be a list'.format(self.ignore_files)
|
|
48
|
-
}
|
|
49
|
+
if not isinstance(self.ignore_files, list):
|
|
50
|
+
raise AnsibleError("The 'ignore_files' option must be a list.", obj=self.ignore_files)
|
|
49
51
|
|
|
50
52
|
def _set_args(self):
|
|
51
53
|
""" Set instance variables based on the arguments that were passed """
|
|
@@ -65,11 +67,8 @@ class ActionModule(ActionBase):
|
|
|
65
67
|
self.ignore_files = self._task.args.get('ignore_files', None)
|
|
66
68
|
self.valid_extensions = self._task.args.get('extensions', self.VALID_FILE_EXTENSIONS)
|
|
67
69
|
|
|
68
|
-
# convert/validate extensions list
|
|
69
|
-
if isinstance(self.valid_extensions, string_types):
|
|
70
|
-
self.valid_extensions = list(self.valid_extensions)
|
|
71
70
|
if not isinstance(self.valid_extensions, list):
|
|
72
|
-
raise AnsibleError('
|
|
71
|
+
raise AnsibleError("The 'extensions' option must be a list.", obj=self.valid_extensions)
|
|
73
72
|
|
|
74
73
|
def run(self, tmp=None, task_vars=None):
|
|
75
74
|
""" Load yml files recursively from a directory.
|
|
@@ -93,10 +92,10 @@ class ActionModule(ActionBase):
|
|
|
93
92
|
elif arg in self.VALID_ALL:
|
|
94
93
|
pass
|
|
95
94
|
else:
|
|
96
|
-
raise AnsibleError('{
|
|
95
|
+
raise AnsibleError(f'{arg} is not a valid option in include_vars', obj=arg)
|
|
97
96
|
|
|
98
97
|
if dirs and files:
|
|
99
|
-
raise AnsibleError("You are mixing file only and dir only arguments, these are incompatible")
|
|
98
|
+
raise AnsibleError("You are mixing file only and dir only arguments, these are incompatible", obj=self._task.args)
|
|
100
99
|
|
|
101
100
|
# set internal vars from args
|
|
102
101
|
self._set_args()
|
|
@@ -108,13 +107,13 @@ class ActionModule(ActionBase):
|
|
|
108
107
|
self._set_root_dir()
|
|
109
108
|
if not path.exists(self.source_dir):
|
|
110
109
|
failed = True
|
|
111
|
-
err_msg =
|
|
110
|
+
err_msg = f"{self.source_dir} directory does not exist"
|
|
112
111
|
elif not path.isdir(self.source_dir):
|
|
113
112
|
failed = True
|
|
114
|
-
err_msg =
|
|
113
|
+
err_msg = f"{self.source_dir} is not a directory"
|
|
115
114
|
else:
|
|
116
115
|
for root_dir, filenames in self._traverse_dir_depth():
|
|
117
|
-
failed, err_msg, updated_results =
|
|
116
|
+
failed, err_msg, updated_results = self._load_files_in_dir(root_dir, filenames)
|
|
118
117
|
if failed:
|
|
119
118
|
break
|
|
120
119
|
results.update(updated_results)
|
|
@@ -175,7 +174,7 @@ class ActionModule(ActionBase):
|
|
|
175
174
|
self.source_dir = path.join(current_dir, self.source_dir)
|
|
176
175
|
|
|
177
176
|
def _log_walk(self, error):
|
|
178
|
-
self._display.vvv(
|
|
177
|
+
self._display.vvv(f"Issue with walking through {error.filename}: {error}")
|
|
179
178
|
|
|
180
179
|
def _traverse_dir_depth(self):
|
|
181
180
|
""" Recursively iterate over a directory and sort the files in
|
|
@@ -204,9 +203,8 @@ class ActionModule(ActionBase):
|
|
|
204
203
|
try:
|
|
205
204
|
if re.search(r'{0}$'.format(file_type), filename):
|
|
206
205
|
return True
|
|
207
|
-
except Exception:
|
|
208
|
-
|
|
209
|
-
raise AnsibleError(err_msg)
|
|
206
|
+
except Exception as ex:
|
|
207
|
+
raise AnsibleError(f'Invalid regular expression: {file_type!r}', obj=file_type) from ex
|
|
210
208
|
return False
|
|
211
209
|
|
|
212
210
|
def _is_valid_file_ext(self, source_file):
|
|
@@ -232,7 +230,7 @@ class ActionModule(ActionBase):
|
|
|
232
230
|
err_msg = ''
|
|
233
231
|
if validate_extensions and not self._is_valid_file_ext(filename):
|
|
234
232
|
failed = True
|
|
235
|
-
err_msg =
|
|
233
|
+
err_msg = f"{filename!r} does not have a valid extension: {', '.join(self.valid_extensions)}"
|
|
236
234
|
else:
|
|
237
235
|
data = self._loader.load_from_file(filename, cache='none', trusted_as_template=True)
|
|
238
236
|
|
|
@@ -243,7 +241,7 @@ class ActionModule(ActionBase):
|
|
|
243
241
|
|
|
244
242
|
if not isinstance(data, dict):
|
|
245
243
|
failed = True
|
|
246
|
-
err_msg =
|
|
244
|
+
err_msg = f"{filename!r} must be stored as a dictionary/hash"
|
|
247
245
|
else:
|
|
248
246
|
self.included_files.append(filename)
|
|
249
247
|
results.update(data)
|
ansible/plugins/action/script.py
CHANGED
|
@@ -139,8 +139,6 @@ class ActionModule(ActionBase):
|
|
|
139
139
|
else:
|
|
140
140
|
script_cmd = ' '.join([env_string, target_command])
|
|
141
141
|
|
|
142
|
-
script_cmd = self._connection._shell.wrap_for_exec(script_cmd)
|
|
143
|
-
|
|
144
142
|
exec_data = None
|
|
145
143
|
# PowerShell runs the script in a special wrapper to enable things
|
|
146
144
|
# like become and environment args
|
|
@@ -149,7 +147,7 @@ class ActionModule(ActionBase):
|
|
|
149
147
|
pc = self._task
|
|
150
148
|
exec_data = ps_manifest._create_powershell_wrapper(
|
|
151
149
|
name=f"ansible.builtin.script.{pathlib.Path(source).stem}",
|
|
152
|
-
module_data=to_bytes(script_cmd),
|
|
150
|
+
module_data=to_bytes(f"& {script_cmd}; exit $LASTEXITCODE"),
|
|
153
151
|
module_path=source,
|
|
154
152
|
module_args={},
|
|
155
153
|
environment=env_dict,
|
|
@@ -23,7 +23,6 @@ from ansible.config.manager import ensure_type
|
|
|
23
23
|
from ansible.errors import AnsibleError, AnsibleActionFail
|
|
24
24
|
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
|
25
25
|
from ansible.module_utils.parsing.convert_bool import boolean
|
|
26
|
-
from ansible.module_utils.six import string_types
|
|
27
26
|
from ansible.plugins.action import ActionBase
|
|
28
27
|
from ansible.template import trust_as_template
|
|
29
28
|
from ansible._internal._templating import _template_vars
|
|
@@ -49,7 +48,7 @@ class ActionModule(ActionBase):
|
|
|
49
48
|
'block_end_string', 'comment_start_string', 'comment_end_string'):
|
|
50
49
|
if s_type in self._task.args:
|
|
51
50
|
value = ensure_type(self._task.args[s_type], 'string')
|
|
52
|
-
if value is not None and not isinstance(value,
|
|
51
|
+
if value is not None and not isinstance(value, str):
|
|
53
52
|
raise AnsibleActionFail("%s is expected to be a string, but got %s instead" % (s_type, type(value)))
|
|
54
53
|
self._task.args[s_type] = value
|
|
55
54
|
|
ansible/plugins/action/uri.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import collections.abc as _c
|
|
9
9
|
import os
|
|
10
|
+
from copy import deepcopy
|
|
10
11
|
|
|
11
12
|
from ansible.errors import AnsibleActionFail
|
|
12
13
|
from ansible.module_utils.parsing.convert_bool import boolean
|
|
@@ -53,7 +54,8 @@ class ActionModule(ActionBase):
|
|
|
53
54
|
raise AnsibleActionFail(
|
|
54
55
|
'body must be mapping, cannot be type %s' % body.__class__.__name__
|
|
55
56
|
)
|
|
56
|
-
|
|
57
|
+
new_body = deepcopy(body)
|
|
58
|
+
for field, value in new_body.items():
|
|
57
59
|
if not isinstance(value, _c.MutableMapping):
|
|
58
60
|
continue
|
|
59
61
|
content = value.get('content')
|
|
@@ -70,7 +72,7 @@ class ActionModule(ActionBase):
|
|
|
70
72
|
value['filename'] = tmp_src
|
|
71
73
|
self._transfer_file(filename, tmp_src)
|
|
72
74
|
self._fixup_perms2((self._connection._shell.tmpdir, tmp_src))
|
|
73
|
-
kwargs['body'] =
|
|
75
|
+
kwargs['body'] = new_body
|
|
74
76
|
|
|
75
77
|
new_module_args = self._task.args | kwargs
|
|
76
78
|
|
|
@@ -162,6 +162,7 @@ class BaseFileCacheModule(BaseCacheModule):
|
|
|
162
162
|
except OSError as ex:
|
|
163
163
|
display.error_as_warning(f"Error in {self.plugin_name!r} cache plugin while trying to write to {tmpfile_path!r}.", exception=ex)
|
|
164
164
|
try:
|
|
165
|
+
os.close(tmpfile_handle) # os.rename fails if handle is still open in WSL
|
|
165
166
|
os.rename(tmpfile_path, cachefile)
|
|
166
167
|
os.chmod(cachefile, mode=S_IRWU_RG_RO)
|
|
167
168
|
except OSError as ex:
|
|
@@ -60,9 +60,6 @@ _YAML_BREAK_CHARS = '\n\x85\u2028\u2029' # NL, NEL, LS, PS
|
|
|
60
60
|
_SPACE_BREAK_RE = re.compile(fr' +([{_YAML_BREAK_CHARS}])')
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
_T_callable = t.TypeVar("_T_callable", bound=t.Callable)
|
|
64
|
-
|
|
65
|
-
|
|
66
63
|
class _AnsibleCallbackDumper(_dumper.AnsibleDumper):
|
|
67
64
|
def __init__(self, *args, lossy: bool = False, **kwargs):
|
|
68
65
|
super().__init__(*args, **kwargs)
|
|
@@ -293,7 +290,11 @@ class CallbackBase(AnsiblePlugin):
|
|
|
293
290
|
)
|
|
294
291
|
|
|
295
292
|
if not indent and any(indent_conditions):
|
|
296
|
-
|
|
293
|
+
try:
|
|
294
|
+
indent = self.get_option('result_indentation')
|
|
295
|
+
except KeyError:
|
|
296
|
+
# Callback does not declare result_indentation nor extend result_format_callback
|
|
297
|
+
indent = 4
|
|
297
298
|
if pretty_results is False:
|
|
298
299
|
# pretty_results=False overrides any specified indentation
|
|
299
300
|
indent = None
|
|
@@ -394,8 +395,14 @@ class CallbackBase(AnsiblePlugin):
|
|
|
394
395
|
# Callback does not declare pretty_results nor extend result_format_callback
|
|
395
396
|
pretty_results = None
|
|
396
397
|
|
|
398
|
+
try:
|
|
399
|
+
indent = self.get_option('result_indentation')
|
|
400
|
+
except KeyError:
|
|
401
|
+
# Callback does not declare result_indentation nor extend result_format_callback
|
|
402
|
+
indent = 4
|
|
403
|
+
|
|
397
404
|
if result_format == 'json':
|
|
398
|
-
return json.dumps(diff, sort_keys=True, indent=
|
|
405
|
+
return json.dumps(diff, sort_keys=True, indent=indent, separators=(u',', u': ')) + u'\n'
|
|
399
406
|
|
|
400
407
|
if result_format == 'yaml':
|
|
401
408
|
# None is a sentinel in this case that indicates default behavior
|
|
@@ -407,7 +414,7 @@ class CallbackBase(AnsiblePlugin):
|
|
|
407
414
|
allow_unicode=True,
|
|
408
415
|
Dumper=functools.partial(_AnsibleCallbackDumper, lossy=lossy),
|
|
409
416
|
default_flow_style=False,
|
|
410
|
-
indent=
|
|
417
|
+
indent=indent,
|
|
411
418
|
# sort_keys=sort_keys # This requires PyYAML>=5.1
|
|
412
419
|
),
|
|
413
420
|
' '
|