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.

Files changed (201) hide show
  1. ansible/_internal/__init__.py +1 -4
  2. ansible/_internal/_ansiballz/_builder.py +1 -3
  3. ansible/_internal/_collection_proxy.py +7 -9
  4. ansible/_internal/_json/__init__.py +3 -4
  5. ansible/_internal/_templating/_engine.py +1 -1
  6. ansible/_internal/_templating/_jinja_plugins.py +1 -2
  7. ansible/_internal/_wrapt.py +105 -301
  8. ansible/cli/__init__.py +11 -10
  9. ansible/cli/adhoc.py +1 -2
  10. ansible/cli/arguments/option_helpers.py +1 -1
  11. ansible/cli/config.py +5 -6
  12. ansible/cli/doc.py +70 -68
  13. ansible/cli/galaxy.py +15 -24
  14. ansible/cli/inventory.py +0 -1
  15. ansible/cli/playbook.py +0 -1
  16. ansible/cli/pull.py +0 -1
  17. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
  18. ansible/collections/list.py +4 -2
  19. ansible/config/base.yml +1 -25
  20. ansible/config/manager.py +0 -2
  21. ansible/executor/play_iterator.py +42 -20
  22. ansible/executor/playbook_executor.py +0 -9
  23. ansible/executor/task_executor.py +26 -18
  24. ansible/executor/task_queue_manager.py +1 -3
  25. ansible/galaxy/api.py +33 -80
  26. ansible/galaxy/collection/__init__.py +4 -17
  27. ansible/galaxy/dependency_resolution/dataclasses.py +0 -10
  28. ansible/galaxy/dependency_resolution/providers.py +24 -118
  29. ansible/galaxy/role.py +1 -33
  30. ansible/inventory/manager.py +2 -3
  31. ansible/keyword_desc.yml +0 -3
  32. ansible/module_utils/_internal/_datatag/__init__.py +2 -10
  33. ansible/module_utils/_internal/_no_six.py +86 -0
  34. ansible/module_utils/_text.py +28 -8
  35. ansible/module_utils/ansible_release.py +2 -2
  36. ansible/module_utils/basic.py +26 -23
  37. ansible/module_utils/common/_collections_compat.py +11 -2
  38. ansible/module_utils/common/collections.py +8 -3
  39. ansible/module_utils/common/dict_transformations.py +1 -2
  40. ansible/module_utils/common/network.py +4 -2
  41. ansible/module_utils/common/parameters.py +32 -41
  42. ansible/module_utils/common/text/converters.py +109 -23
  43. ansible/module_utils/common/text/formatters.py +6 -2
  44. ansible/module_utils/common/validation.py +11 -9
  45. ansible/module_utils/connection.py +8 -3
  46. ansible/module_utils/facts/hardware/linux.py +23 -7
  47. ansible/module_utils/facts/hardware/netbsd.py +1 -1
  48. ansible/module_utils/facts/hardware/sunos.py +2 -1
  49. ansible/module_utils/facts/packages.py +6 -2
  50. ansible/module_utils/facts/system/distribution.py +2 -1
  51. ansible/module_utils/facts/system/env.py +6 -3
  52. ansible/module_utils/facts/system/local.py +3 -1
  53. ansible/module_utils/parsing/convert_bool.py +6 -2
  54. ansible/module_utils/service.py +2 -3
  55. ansible/module_utils/six/__init__.py +19 -6
  56. ansible/module_utils/yumdnf.py +0 -5
  57. ansible/modules/apt.py +18 -13
  58. ansible/modules/apt_repository.py +1 -1
  59. ansible/modules/assemble.py +5 -9
  60. ansible/modules/blockinfile.py +39 -23
  61. ansible/modules/cron.py +26 -35
  62. ansible/modules/deb822_repository.py +83 -12
  63. ansible/modules/dnf.py +3 -7
  64. ansible/modules/dnf5.py +4 -6
  65. ansible/modules/expect.py +0 -3
  66. ansible/modules/find.py +1 -2
  67. ansible/modules/get_url.py +1 -1
  68. ansible/modules/git.py +4 -5
  69. ansible/modules/include_vars.py +1 -1
  70. ansible/modules/known_hosts.py +7 -1
  71. ansible/modules/lineinfile.py +71 -63
  72. ansible/modules/package_facts.py +1 -1
  73. ansible/modules/pip.py +8 -2
  74. ansible/modules/replace.py +6 -6
  75. ansible/modules/service.py +3 -4
  76. ansible/modules/stat.py +20 -0
  77. ansible/modules/uri.py +9 -10
  78. ansible/modules/user.py +1 -2
  79. ansible/modules/wait_for.py +2 -2
  80. ansible/modules/wait_for_connection.py +2 -1
  81. ansible/modules/yum_repository.py +1 -16
  82. ansible/parsing/dataloader.py +24 -31
  83. ansible/parsing/mod_args.py +3 -0
  84. ansible/parsing/vault/__init__.py +1 -2
  85. ansible/playbook/base.py +8 -56
  86. ansible/playbook/block.py +1 -63
  87. ansible/playbook/collectionsearch.py +1 -2
  88. ansible/playbook/handler.py +1 -7
  89. ansible/playbook/helpers.py +15 -20
  90. ansible/playbook/included_file.py +1 -1
  91. ansible/playbook/play.py +105 -49
  92. ansible/playbook/play_context.py +4 -0
  93. ansible/playbook/role/__init__.py +10 -65
  94. ansible/playbook/role/definition.py +3 -4
  95. ansible/playbook/role/include.py +2 -3
  96. ansible/playbook/role/metadata.py +1 -12
  97. ansible/playbook/role/requirement.py +1 -2
  98. ansible/playbook/role_include.py +1 -2
  99. ansible/playbook/taggable.py +16 -5
  100. ansible/playbook/task.py +51 -55
  101. ansible/plugins/action/__init__.py +20 -19
  102. ansible/plugins/action/add_host.py +1 -2
  103. ansible/plugins/action/fetch.py +3 -5
  104. ansible/plugins/action/group_by.py +1 -2
  105. ansible/plugins/action/include_vars.py +20 -22
  106. ansible/plugins/action/script.py +1 -3
  107. ansible/plugins/action/template.py +1 -2
  108. ansible/plugins/action/uri.py +4 -2
  109. ansible/plugins/cache/__init__.py +1 -0
  110. ansible/plugins/callback/__init__.py +13 -6
  111. ansible/plugins/connection/__init__.py +3 -7
  112. ansible/plugins/connection/local.py +2 -3
  113. ansible/plugins/connection/psrp.py +0 -2
  114. ansible/plugins/connection/ssh.py +2 -7
  115. ansible/plugins/connection/winrm.py +0 -2
  116. ansible/plugins/doc_fragments/result_format_callback.py +15 -0
  117. ansible/plugins/filter/core.py +4 -5
  118. ansible/plugins/filter/encryption.py +3 -27
  119. ansible/plugins/filter/mathstuff.py +1 -2
  120. ansible/plugins/filter/to_nice_yaml.yml +31 -3
  121. ansible/plugins/filter/to_yaml.yml +29 -12
  122. ansible/plugins/inventory/__init__.py +1 -2
  123. ansible/plugins/inventory/toml.py +3 -6
  124. ansible/plugins/inventory/yaml.py +1 -2
  125. ansible/plugins/loader.py +3 -4
  126. ansible/plugins/lookup/password.py +1 -2
  127. ansible/plugins/lookup/subelements.py +2 -3
  128. ansible/plugins/lookup/url.py +1 -1
  129. ansible/plugins/lookup/varnames.py +1 -2
  130. ansible/plugins/shell/__init__.py +9 -4
  131. ansible/plugins/shell/powershell.py +8 -24
  132. ansible/plugins/strategy/__init__.py +6 -3
  133. ansible/plugins/test/core.py +4 -1
  134. ansible/plugins/test/falsy.yml +1 -1
  135. ansible/plugins/test/regex.yml +18 -6
  136. ansible/plugins/test/truthy.yml +1 -1
  137. ansible/release.py +2 -2
  138. ansible/template/__init__.py +3 -7
  139. ansible/utils/collection_loader/_collection_config.py +5 -0
  140. ansible/utils/collection_loader/_collection_finder.py +11 -14
  141. ansible/utils/context_objects.py +7 -4
  142. ansible/utils/display.py +7 -6
  143. ansible/utils/encrypt.py +0 -5
  144. ansible/utils/helpers.py +6 -2
  145. ansible/utils/jsonrpc.py +7 -3
  146. ansible/utils/plugin_docs.py +49 -38
  147. ansible/utils/ssh_functions.py +0 -19
  148. ansible/utils/unsafe_proxy.py +7 -7
  149. ansible/vars/clean.py +2 -3
  150. ansible/vars/manager.py +28 -22
  151. ansible/vars/plugins.py +1 -31
  152. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/METADATA +3 -3
  153. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/RECORD +199 -200
  154. ansible_test/_data/completion/docker.txt +7 -7
  155. ansible_test/_data/completion/network.txt +0 -1
  156. ansible_test/_data/completion/remote.txt +4 -4
  157. ansible_test/_data/requirements/ansible-test.txt +1 -1
  158. ansible_test/_data/requirements/sanity.changelog.txt +1 -1
  159. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  160. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  161. ansible_test/_internal/cache.py +2 -5
  162. ansible_test/_internal/cli/compat.py +1 -1
  163. ansible_test/_internal/commands/coverage/combine.py +1 -3
  164. ansible_test/_internal/commands/integration/__init__.py +3 -7
  165. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  166. ansible_test/_internal/commands/integration/coverage.py +1 -3
  167. ansible_test/_internal/commands/integration/filters.py +5 -10
  168. ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
  169. ansible_test/_internal/commands/units/__init__.py +1 -13
  170. ansible_test/_internal/completion.py +2 -5
  171. ansible_test/_internal/config.py +2 -7
  172. ansible_test/_internal/coverage_util.py +1 -1
  173. ansible_test/_internal/delegation.py +2 -0
  174. ansible_test/_internal/docker_util.py +1 -1
  175. ansible_test/_internal/host_profiles.py +6 -11
  176. ansible_test/_internal/provider/__init__.py +2 -5
  177. ansible_test/_internal/provisioning.py +2 -5
  178. ansible_test/_internal/pypi_proxy.py +1 -1
  179. ansible_test/_internal/target.py +2 -6
  180. ansible_test/_internal/thread.py +1 -4
  181. ansible_test/_internal/util.py +9 -14
  182. ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
  183. ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +40 -27
  184. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
  185. ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
  186. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
  187. ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
  188. ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
  189. ansible_test/_util/target/common/constants.py +2 -2
  190. ansible_test/_util/target/setup/bootstrap.sh +0 -6
  191. ansible/utils/py3compat.py +0 -27
  192. ansible_test/_data/pytest/config/legacy.ini +0 -4
  193. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/WHEEL +0 -0
  194. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/entry_points.txt +0 -0
  195. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/COPYING +0 -0
  196. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  197. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  198. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  199. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  200. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  201. {ansible_core-2.19.3rc1.dist-info → ansible_core-2.20.0b2.dist-info}/top_level.txt +0 -0
@@ -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._internal._templating._engine import TemplateEngine
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
- return ds
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 isinstance(ds, str):
49
- return [AnsibleTagHelper.tag_copy(ds, item.strip()) for item in ds.split(',')]
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
- raise AnsibleError('tags must be specified as a list', obj=ds)
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.resolved_action: str | None = None
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, string_types):
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.resolved_action = module_or_action_context.resolved_fqcn
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.resolved_action = self.resolved_action
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(resolved_action=self.resolved_action)
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, text_type):
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
- if res['rc'] == 0:
684
- return remote_paths
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'], string_types):
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(text_type(v)))
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'], string_types)):
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, binary_type):
1326
+ if isinstance(stdout, bytes):
1326
1327
  out = to_text(stdout, errors=encoding_errors)
1327
- elif not isinstance(stdout, text_type):
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, binary_type):
1333
+ if isinstance(stderr, bytes):
1333
1334
  err = to_text(stderr, errors=encoding_errors)
1334
- elif not isinstance(stderr, text_type):
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, string_types):
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)
@@ -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, string_types):
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, string_types):
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, string_types):
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, string_types):
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
- elif isinstance(self.ignore_files, dict):
45
- return {
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('Invalid type for "extensions" option, it must be a list')
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('{0} is not a valid option in include_vars'.format(to_native(arg)))
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 = ('{0} directory does not exist'.format(to_native(self.source_dir)))
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 = ('{0} is not a directory'.format(to_native(self.source_dir)))
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 = (self._load_files_in_dir(root_dir, filenames))
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('Issue with walking through "%s": %s' % (to_native(error.filename), to_native(error)))
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
- err_msg = 'Invalid regular expression: {0}'.format(file_type)
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 = ('{0} does not have a valid extension: {1}'.format(to_native(filename), ', '.join(self.valid_extensions)))
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 = ('{0} must be stored as a dictionary/hash'.format(to_native(filename)))
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)
@@ -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, string_types):
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
 
@@ -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
- for field, value in body.items():
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'] = 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
- indent = 4
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=4, separators=(u',', u': ')) + u'\n'
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=4,
417
+ indent=indent,
411
418
  # sort_keys=sort_keys # This requires PyYAML>=5.1
412
419
  ),
413
420
  ' '