ansible-core 2.19.2__py3-none-any.whl → 2.20.0b1__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 (202) 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/_display_utils.py +145 -0
  5. ansible/_internal/_json/__init__.py +3 -4
  6. ansible/_internal/_templating/_engine.py +1 -1
  7. ansible/_internal/_templating/_jinja_plugins.py +1 -2
  8. ansible/_internal/_wrapt.py +105 -301
  9. ansible/cli/__init__.py +11 -10
  10. ansible/cli/adhoc.py +1 -2
  11. ansible/cli/arguments/option_helpers.py +1 -1
  12. ansible/cli/config.py +5 -6
  13. ansible/cli/doc.py +67 -67
  14. ansible/cli/galaxy.py +15 -24
  15. ansible/cli/inventory.py +0 -1
  16. ansible/cli/playbook.py +0 -1
  17. ansible/cli/pull.py +0 -1
  18. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
  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/powershell/async_watchdog.ps1 +24 -4
  24. ansible/executor/task_executor.py +32 -22
  25. ansible/executor/task_queue_manager.py +1 -3
  26. ansible/galaxy/api.py +33 -80
  27. ansible/galaxy/collection/__init__.py +4 -17
  28. ansible/galaxy/dependency_resolution/dataclasses.py +0 -10
  29. ansible/galaxy/dependency_resolution/providers.py +1 -2
  30. ansible/galaxy/role.py +1 -33
  31. ansible/inventory/manager.py +2 -3
  32. ansible/keyword_desc.yml +0 -3
  33. ansible/module_utils/_internal/_datatag/__init__.py +2 -10
  34. ansible/module_utils/_internal/_no_six.py +86 -0
  35. ansible/module_utils/_text.py +28 -8
  36. ansible/module_utils/ansible_release.py +2 -2
  37. ansible/module_utils/basic.py +27 -24
  38. ansible/module_utils/common/_collections_compat.py +11 -2
  39. ansible/module_utils/common/collections.py +8 -3
  40. ansible/module_utils/common/dict_transformations.py +1 -2
  41. ansible/module_utils/common/network.py +4 -2
  42. ansible/module_utils/common/parameters.py +32 -41
  43. ansible/module_utils/common/text/converters.py +109 -23
  44. ansible/module_utils/common/text/formatters.py +6 -2
  45. ansible/module_utils/common/validation.py +11 -9
  46. ansible/module_utils/connection.py +8 -3
  47. ansible/module_utils/facts/hardware/linux.py +23 -7
  48. ansible/module_utils/facts/hardware/netbsd.py +1 -1
  49. ansible/module_utils/facts/hardware/sunos.py +2 -1
  50. ansible/module_utils/facts/packages.py +6 -2
  51. ansible/module_utils/facts/system/distribution.py +2 -1
  52. ansible/module_utils/facts/system/env.py +6 -3
  53. ansible/module_utils/facts/system/local.py +3 -1
  54. ansible/module_utils/parsing/convert_bool.py +6 -2
  55. ansible/module_utils/service.py +2 -3
  56. ansible/module_utils/six/__init__.py +11 -6
  57. ansible/module_utils/urls.py +6 -2
  58. ansible/module_utils/yumdnf.py +0 -5
  59. ansible/modules/apt.py +18 -13
  60. ansible/modules/apt_repository.py +1 -1
  61. ansible/modules/assemble.py +5 -9
  62. ansible/modules/blockinfile.py +39 -23
  63. ansible/modules/cron.py +26 -35
  64. ansible/modules/deb822_repository.py +83 -12
  65. ansible/modules/dnf.py +3 -7
  66. ansible/modules/dnf5.py +4 -6
  67. ansible/modules/expect.py +0 -3
  68. ansible/modules/find.py +1 -2
  69. ansible/modules/get_url.py +1 -1
  70. ansible/modules/git.py +4 -5
  71. ansible/modules/include_vars.py +1 -1
  72. ansible/modules/lineinfile.py +71 -63
  73. ansible/modules/package_facts.py +1 -1
  74. ansible/modules/pip.py +8 -2
  75. ansible/modules/replace.py +6 -6
  76. ansible/modules/service.py +3 -4
  77. ansible/modules/stat.py +20 -0
  78. ansible/modules/uri.py +9 -10
  79. ansible/modules/user.py +1 -2
  80. ansible/modules/wait_for.py +2 -2
  81. ansible/modules/wait_for_connection.py +2 -1
  82. ansible/modules/yum_repository.py +1 -16
  83. ansible/parsing/dataloader.py +24 -31
  84. ansible/parsing/mod_args.py +3 -0
  85. ansible/parsing/vault/__init__.py +1 -2
  86. ansible/playbook/base.py +8 -56
  87. ansible/playbook/block.py +0 -60
  88. ansible/playbook/collectionsearch.py +1 -2
  89. ansible/playbook/handler.py +1 -7
  90. ansible/playbook/helpers.py +0 -7
  91. ansible/playbook/included_file.py +1 -1
  92. ansible/playbook/play.py +103 -37
  93. ansible/playbook/play_context.py +4 -0
  94. ansible/playbook/role/__init__.py +10 -65
  95. ansible/playbook/role/definition.py +3 -4
  96. ansible/playbook/role/include.py +2 -3
  97. ansible/playbook/role/metadata.py +1 -12
  98. ansible/playbook/role/requirement.py +1 -2
  99. ansible/playbook/role_include.py +1 -2
  100. ansible/playbook/taggable.py +16 -5
  101. ansible/playbook/task.py +51 -55
  102. ansible/plugins/action/__init__.py +20 -19
  103. ansible/plugins/action/add_host.py +1 -2
  104. ansible/plugins/action/fetch.py +2 -4
  105. ansible/plugins/action/group_by.py +1 -2
  106. ansible/plugins/action/include_vars.py +20 -22
  107. ansible/plugins/action/script.py +1 -3
  108. ansible/plugins/action/template.py +1 -2
  109. ansible/plugins/action/uri.py +4 -2
  110. ansible/plugins/cache/__init__.py +1 -0
  111. ansible/plugins/callback/__init__.py +13 -6
  112. ansible/plugins/connection/__init__.py +3 -7
  113. ansible/plugins/connection/local.py +2 -3
  114. ansible/plugins/connection/psrp.py +0 -2
  115. ansible/plugins/connection/ssh.py +2 -7
  116. ansible/plugins/connection/winrm.py +0 -2
  117. ansible/plugins/doc_fragments/result_format_callback.py +15 -0
  118. ansible/plugins/filter/core.py +4 -5
  119. ansible/plugins/filter/encryption.py +3 -27
  120. ansible/plugins/filter/mathstuff.py +1 -2
  121. ansible/plugins/filter/to_nice_yaml.yml +31 -3
  122. ansible/plugins/filter/to_yaml.yml +29 -12
  123. ansible/plugins/inventory/__init__.py +1 -2
  124. ansible/plugins/inventory/script.py +2 -1
  125. ansible/plugins/inventory/toml.py +3 -6
  126. ansible/plugins/inventory/yaml.py +1 -2
  127. ansible/plugins/list.py +10 -3
  128. ansible/plugins/loader.py +6 -6
  129. ansible/plugins/lookup/password.py +1 -2
  130. ansible/plugins/lookup/subelements.py +2 -3
  131. ansible/plugins/lookup/url.py +1 -1
  132. ansible/plugins/lookup/varnames.py +1 -2
  133. ansible/plugins/shell/__init__.py +9 -4
  134. ansible/plugins/shell/powershell.py +8 -24
  135. ansible/plugins/strategy/__init__.py +6 -3
  136. ansible/plugins/test/core.py +4 -1
  137. ansible/plugins/test/regex.yml +18 -6
  138. ansible/release.py +2 -2
  139. ansible/template/__init__.py +3 -7
  140. ansible/utils/collection_loader/_collection_config.py +5 -0
  141. ansible/utils/collection_loader/_collection_finder.py +11 -14
  142. ansible/utils/context_objects.py +7 -4
  143. ansible/utils/display.py +28 -167
  144. ansible/utils/encrypt.py +0 -5
  145. ansible/utils/helpers.py +6 -2
  146. ansible/utils/jsonrpc.py +7 -3
  147. ansible/utils/plugin_docs.py +49 -38
  148. ansible/utils/ssh_functions.py +0 -19
  149. ansible/utils/unsafe_proxy.py +7 -7
  150. ansible/vars/clean.py +2 -3
  151. ansible/vars/manager.py +27 -20
  152. ansible/vars/plugins.py +1 -31
  153. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/METADATA +3 -3
  154. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/RECORD +200 -200
  155. ansible_test/_data/completion/docker.txt +7 -7
  156. ansible_test/_data/completion/network.txt +0 -1
  157. ansible_test/_data/completion/remote.txt +4 -4
  158. ansible_test/_data/requirements/ansible-test.txt +1 -1
  159. ansible_test/_data/requirements/sanity.changelog.txt +1 -1
  160. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  161. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  162. ansible_test/_internal/cache.py +2 -5
  163. ansible_test/_internal/cli/compat.py +1 -1
  164. ansible_test/_internal/commands/coverage/combine.py +1 -3
  165. ansible_test/_internal/commands/integration/__init__.py +3 -7
  166. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  167. ansible_test/_internal/commands/integration/coverage.py +1 -3
  168. ansible_test/_internal/commands/integration/filters.py +5 -10
  169. ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
  170. ansible_test/_internal/commands/units/__init__.py +1 -13
  171. ansible_test/_internal/completion.py +2 -5
  172. ansible_test/_internal/config.py +2 -7
  173. ansible_test/_internal/coverage_util.py +1 -1
  174. ansible_test/_internal/delegation.py +2 -0
  175. ansible_test/_internal/docker_util.py +1 -1
  176. ansible_test/_internal/host_profiles.py +6 -11
  177. ansible_test/_internal/provider/__init__.py +2 -5
  178. ansible_test/_internal/provisioning.py +2 -5
  179. ansible_test/_internal/pypi_proxy.py +1 -1
  180. ansible_test/_internal/target.py +2 -6
  181. ansible_test/_internal/thread.py +1 -4
  182. ansible_test/_internal/util.py +9 -14
  183. ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
  184. ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +30 -27
  185. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
  186. ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
  187. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
  188. ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
  189. ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
  190. ansible_test/_util/target/common/constants.py +2 -2
  191. ansible_test/_util/target/setup/bootstrap.sh +0 -6
  192. ansible/utils/py3compat.py +0 -27
  193. ansible_test/_data/pytest/config/legacy.ini +0 -4
  194. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/WHEEL +0 -0
  195. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/entry_points.txt +0 -0
  196. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/COPYING +0 -0
  197. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  198. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  199. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  200. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  201. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  202. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/top_level.txt +0 -0
ansible/playbook/play.py CHANGED
@@ -17,12 +17,15 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import functools as _functools
21
+ import pathlib as _pathlib
22
+
20
23
  from ansible import constants as C
21
24
  from ansible import context
22
25
  from ansible.errors import AnsibleError
23
- from ansible.errors import AnsibleParserError, AnsibleAssertionError
26
+ from ansible.errors import AnsibleParserError, AnsibleAssertionError, AnsibleValueOmittedError
24
27
  from ansible.module_utils.common.collections import is_sequence
25
- from ansible.module_utils.six import binary_type, string_types, text_type
28
+ from ansible.module_utils.common.yaml import yaml_dump
26
29
  from ansible.playbook.attribute import NonInheritableFieldAttribute
27
30
  from ansible.playbook.base import Base
28
31
  from ansible.playbook.block import Block
@@ -34,6 +37,8 @@ from ansible.playbook.taggable import Taggable
34
37
  from ansible.parsing.vault import EncryptedString
35
38
  from ansible.utils.display import Display
36
39
 
40
+ from ansible._internal._templating._engine import TemplateEngine as _TE
41
+
37
42
  display = Display()
38
43
 
39
44
 
@@ -53,11 +58,11 @@ class Play(Base, Taggable, CollectionSearch):
53
58
  """
54
59
 
55
60
  # =================================================================================
56
- hosts = NonInheritableFieldAttribute(isa='list', required=True, listof=string_types, always_post_validate=True, priority=-2)
61
+ hosts = NonInheritableFieldAttribute(isa='list', required=True, listof=(str,), always_post_validate=True, priority=-2)
57
62
 
58
63
  # Facts
59
64
  gather_facts = NonInheritableFieldAttribute(isa='bool', default=None, always_post_validate=True)
60
- gather_subset = NonInheritableFieldAttribute(isa='list', default=None, listof=string_types, always_post_validate=True)
65
+ gather_subset = NonInheritableFieldAttribute(isa='list', default=None, listof=(str,), always_post_validate=True)
61
66
  gather_timeout = NonInheritableFieldAttribute(isa='int', default=None, always_post_validate=True)
62
67
  fact_path = NonInheritableFieldAttribute(isa='string', default=None)
63
68
 
@@ -65,6 +70,8 @@ class Play(Base, Taggable, CollectionSearch):
65
70
  vars_files = NonInheritableFieldAttribute(isa='list', default=list, priority=99)
66
71
  vars_prompt = NonInheritableFieldAttribute(isa='list', default=list, always_post_validate=False)
67
72
 
73
+ validate_argspec = NonInheritableFieldAttribute(isa='string', always_post_validate=True)
74
+
68
75
  # Role Attributes
69
76
  roles = NonInheritableFieldAttribute(isa='list', default=list, priority=90)
70
77
 
@@ -120,10 +127,10 @@ class Play(Base, Taggable, CollectionSearch):
120
127
  for entry in value:
121
128
  if entry is None:
122
129
  raise AnsibleParserError("Hosts list cannot contain values of 'None'. Please check your playbook")
123
- elif not isinstance(entry, (binary_type, text_type)):
130
+ elif not isinstance(entry, (bytes, str)):
124
131
  raise AnsibleParserError("Hosts list contains an invalid host value: '{host!s}'".format(host=entry))
125
132
 
126
- elif not isinstance(value, (binary_type, text_type, EncryptedString)):
133
+ elif not isinstance(value, (bytes, str, EncryptedString)):
127
134
  raise AnsibleParserError("Hosts list must be a sequence or string. Please check your playbook.")
128
135
 
129
136
  def get_name(self):
@@ -303,7 +310,7 @@ class Play(Base, Taggable, CollectionSearch):
303
310
 
304
311
  t = Task(block=flush_block)
305
312
  t.action = 'meta'
306
- t.resolved_action = 'ansible.builtin.meta'
313
+ t._resolved_action = 'ansible.builtin.meta'
307
314
  t.args['_raw_params'] = 'flush_handlers'
308
315
  t.implicit = True
309
316
  t.set_loader(self._loader)
@@ -400,36 +407,6 @@ class Play(Base, Taggable, CollectionSearch):
400
407
  tasklist.append(task)
401
408
  return tasklist
402
409
 
403
- def serialize(self):
404
- data = super(Play, self).serialize()
405
-
406
- roles = []
407
- for role in self.get_roles():
408
- roles.append(role.serialize())
409
- data['roles'] = roles
410
- data['included_path'] = self._included_path
411
- data['action_groups'] = self._action_groups
412
- data['group_actions'] = self._group_actions
413
-
414
- return data
415
-
416
- def deserialize(self, data):
417
- super(Play, self).deserialize(data)
418
-
419
- self._included_path = data.get('included_path', None)
420
- self._action_groups = data.get('action_groups', {})
421
- self._group_actions = data.get('group_actions', {})
422
- if 'roles' in data:
423
- role_data = data.get('roles', [])
424
- roles = []
425
- for role in role_data:
426
- r = Role()
427
- r.deserialize(role)
428
- roles.append(r)
429
-
430
- setattr(self, 'roles', roles)
431
- del data['roles']
432
-
433
410
  def copy(self):
434
411
  new_me = super(Play, self).copy()
435
412
  new_me.role_cache = self.role_cache.copy()
@@ -438,3 +415,92 @@ class Play(Base, Taggable, CollectionSearch):
438
415
  new_me._action_groups = self._action_groups
439
416
  new_me._group_actions = self._group_actions
440
417
  return new_me
418
+
419
+ def _post_validate_validate_argspec(self, attr: NonInheritableFieldAttribute, value: object, templar: _TE) -> str | None:
420
+ """Validate user input is a bool or string, and return the corresponding argument spec name."""
421
+
422
+ # Ensure the configuration is valid
423
+ if isinstance(value, str):
424
+ try:
425
+ value = templar.template(value)
426
+ except AnsibleValueOmittedError:
427
+ value = False
428
+
429
+ if not isinstance(value, (str, bool)):
430
+ raise AnsibleParserError(f"validate_argspec must be a boolean or string, not {type(value)}", obj=value)
431
+
432
+ # Short-circuit if configuration is turned off or inapplicable
433
+ if not value or self._origin is None:
434
+ return None
435
+
436
+ # Use the requested argument spec or fall back to the play name
437
+ argspec_name = None
438
+ if isinstance(value, str):
439
+ argspec_name = value
440
+ elif self._ds.get("name"):
441
+ argspec_name = self.name
442
+
443
+ metadata_err = argspec_err = ""
444
+ if not argspec_name:
445
+ argspec_err = (
446
+ "A play name is required when validate_argspec is True. "
447
+ "Alternatively, set validate_argspec to the name of an argument spec."
448
+ )
449
+ if self._metadata_path is None:
450
+ metadata_err = "A playbook meta file is required. Considered:\n - "
451
+ metadata_err += "\n - ".join([path.as_posix() for path in self._metadata_candidate_paths])
452
+
453
+ if metadata_err or argspec_err:
454
+ error = f"{argspec_err + (' ' if argspec_err else '')}{metadata_err}"
455
+ raise AnsibleParserError(error, obj=self._origin)
456
+
457
+ metadata = self._loader.load_from_file(self._metadata_path)
458
+
459
+ try:
460
+ metadata = metadata['argument_specs']
461
+ metadata = metadata[argspec_name]
462
+ options = metadata['options']
463
+ except (TypeError, KeyError):
464
+ options = None
465
+
466
+ if not isinstance(options, dict):
467
+ raise AnsibleParserError(
468
+ f"No argument spec named '{argspec_name}' in {self._metadata_path}. Minimally expected:\n"
469
+ + yaml_dump({"argument_specs": {f"{argspec_name!s}": {"options": {}}}}),
470
+ obj=metadata,
471
+ )
472
+
473
+ return argspec_name
474
+
475
+ @property
476
+ def _metadata_candidate_paths(self) -> list[_pathlib.Path]:
477
+ """A list of possible playbook.meta paths in configured order."""
478
+ extensions = C.config.get_config_value("YAML_FILENAME_EXTENSIONS")
479
+ if self._origin.path.endswith(tuple(extensions)):
480
+ playbook_without_ext = self._origin.path.rsplit('.', 1)[0]
481
+ else:
482
+ playbook_without_ext = self._origin.path
483
+
484
+ return [_pathlib.Path(playbook_without_ext + ".meta" + ext) for ext in extensions + ['']]
485
+
486
+ @_functools.cached_property
487
+ def _metadata_path(self) -> str | None:
488
+ """Locate playbook meta path:
489
+
490
+ playbook{ext?} -> playbook.meta{ext?}
491
+ """
492
+ if self._origin is None:
493
+ # adhoc, ansible-console don't have an associated playbook
494
+ return None
495
+ for candidate in self._metadata_candidate_paths:
496
+ if candidate.is_file():
497
+ return candidate.as_posix()
498
+ return None
499
+
500
+ @property
501
+ def argument_spec(self) -> dict:
502
+ """Retrieve the argument spec if one is configured."""
503
+ if not self.validate_argspec:
504
+ return {}
505
+
506
+ return self._loader.load_from_file(self._metadata_path)['argument_specs'][self.validate_argspec]['options']
@@ -325,3 +325,7 @@ class PlayContext(Base):
325
325
  variables[var_opt] = var_val
326
326
  except AttributeError:
327
327
  continue
328
+
329
+ def deserialize(self, data):
330
+ """Do not use this method. Backward compatibility for network connections plugins that rely on it."""
331
+ self.from_attrs(data)
@@ -27,7 +27,6 @@ from ansible import constants as C
27
27
  from ansible.errors import AnsibleError, AnsibleParserError, AnsibleAssertionError
28
28
  from ansible.module_utils.common.sentinel import Sentinel
29
29
  from ansible.module_utils.common.text.converters import to_text
30
- from ansible.module_utils.six import binary_type, text_type
31
30
  from ansible.playbook.base import Base
32
31
  from ansible.playbook.collectionsearch import CollectionSearch
33
32
  from ansible.playbook.conditional import Conditional
@@ -37,6 +36,7 @@ from ansible.playbook.role.metadata import RoleMetadata
37
36
  from ansible.playbook.taggable import Taggable
38
37
  from ansible.plugins.loader import add_all_plugin_dirs
39
38
  from ansible.utils.collection_loader import AnsibleCollectionConfig
39
+ from ansible.utils.display import Display
40
40
  from ansible.utils.path import is_subpath
41
41
  from ansible.utils.vars import combine_vars
42
42
 
@@ -52,14 +52,12 @@ if _t.TYPE_CHECKING:
52
52
 
53
53
  __all__ = ['Role', 'hash_params']
54
54
 
55
- # TODO: this should be a utility function, but can't be a member of
56
- # the role due to the fact that it would require the use of self
57
- # in a static method. This is also used in the base class for
58
- # strategies (ansible/plugins/strategy/__init__.py)
55
+ _display = Display()
59
56
 
60
57
 
61
58
  def hash_params(params):
62
59
  """
60
+ DEPRECATED
63
61
  Construct a data structure of parameters that is hashable.
64
62
 
65
63
  This requires changing any mutable data structures into immutable ones.
@@ -71,10 +69,16 @@ def hash_params(params):
71
69
  1) There shouldn't be any unhashable scalars specified in the yaml
72
70
  2) Our only choice would be to return an error anyway.
73
71
  """
72
+
73
+ _display.deprecated(
74
+ msg="The hash_params function is deprecated as its consumers have moved to internal alternatives",
75
+ version='2.24',
76
+ help_text='Contact the plugin author to update their code',
77
+ )
74
78
  # Any container is unhashable if it contains unhashable items (for
75
79
  # instance, tuple() is a Hashable subclass but if it contains a dict, it
76
80
  # cannot be hashed)
77
- if isinstance(params, Container) and not isinstance(params, (text_type, binary_type)):
81
+ if isinstance(params, Container) and not isinstance(params, (str, bytes)):
78
82
  if isinstance(params, Mapping):
79
83
  try:
80
84
  # Optimistically hope the contents are all hashable
@@ -651,65 +655,6 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
651
655
 
652
656
  return block_list
653
657
 
654
- def serialize(self, include_deps=True):
655
- res = super(Role, self).serialize()
656
-
657
- res['_role_name'] = self._role_name
658
- res['_role_path'] = self._role_path
659
- res['_role_vars'] = self._role_vars
660
- res['_role_params'] = self._role_params
661
- res['_default_vars'] = self._default_vars
662
- res['_had_task_run'] = self._had_task_run.copy()
663
- res['_completed'] = self._completed.copy()
664
-
665
- res['_metadata'] = self._metadata.serialize()
666
-
667
- if include_deps:
668
- deps = []
669
- for role in self.get_direct_dependencies():
670
- deps.append(role.serialize())
671
- res['_dependencies'] = deps
672
-
673
- parents = []
674
- for parent in self._parents:
675
- parents.append(parent.serialize(include_deps=False))
676
- res['_parents'] = parents
677
-
678
- return res
679
-
680
- def deserialize(self, data, include_deps=True):
681
- self._role_name = data.get('_role_name', '')
682
- self._role_path = data.get('_role_path', '')
683
- self._role_vars = data.get('_role_vars', dict())
684
- self._role_params = data.get('_role_params', dict())
685
- self._default_vars = data.get('_default_vars', dict())
686
- self._had_task_run = data.get('_had_task_run', dict())
687
- self._completed = data.get('_completed', dict())
688
-
689
- if include_deps:
690
- deps = []
691
- for dep in data.get('_dependencies', []):
692
- r = Role()
693
- r.deserialize(dep)
694
- deps.append(r)
695
- setattr(self, '_dependencies', deps)
696
-
697
- parent_data = data.get('_parents', [])
698
- parents = []
699
- for parent in parent_data:
700
- r = Role()
701
- r.deserialize(parent, include_deps=False)
702
- parents.append(r)
703
- setattr(self, '_parents', parents)
704
-
705
- metadata_data = data.get('_metadata')
706
- if metadata_data:
707
- m = RoleMetadata()
708
- m.deserialize(metadata_data)
709
- self._metadata = m
710
-
711
- super(Role, self).deserialize(data)
712
-
713
658
  def set_loader(self, loader):
714
659
  self._loader = loader
715
660
  for parent in self._parents:
@@ -22,7 +22,6 @@ import os
22
22
  from ansible import constants as C
23
23
  from ansible.errors import AnsibleError, AnsibleAssertionError
24
24
  from ansible.module_utils._internal._datatag import AnsibleTagHelper
25
- from ansible.module_utils.six import string_types
26
25
  from ansible.playbook.attribute import NonInheritableFieldAttribute
27
26
  from ansible.playbook.base import Base
28
27
  from ansible.playbook.collectionsearch import CollectionSearch
@@ -70,7 +69,7 @@ class RoleDefinition(Base, Conditional, Taggable, CollectionSearch):
70
69
  if isinstance(ds, int):
71
70
  ds = "%s" % ds
72
71
 
73
- if not isinstance(ds, dict) and not isinstance(ds, string_types):
72
+ if not isinstance(ds, dict) and not isinstance(ds, str):
74
73
  raise AnsibleAssertionError()
75
74
 
76
75
  if isinstance(ds, dict):
@@ -113,11 +112,11 @@ class RoleDefinition(Base, Conditional, Taggable, CollectionSearch):
113
112
  string), just that string
114
113
  """
115
114
 
116
- if isinstance(ds, string_types):
115
+ if isinstance(ds, str):
117
116
  return ds
118
117
 
119
118
  role_name = ds.get('role', ds.get('name'))
120
- if not role_name or not isinstance(role_name, string_types):
119
+ if not role_name or not isinstance(role_name, str):
121
120
  raise AnsibleError('role definitions must contain a role name', obj=ds)
122
121
 
123
122
  # if we have the required datastructures, and if the role_name
@@ -18,7 +18,6 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  from ansible.errors import AnsibleError, AnsibleParserError
21
- from ansible.module_utils.six import string_types
22
21
  from ansible.playbook.delegatable import Delegatable
23
22
  from ansible.playbook.role.definition import RoleDefinition
24
23
 
@@ -40,10 +39,10 @@ class RoleInclude(RoleDefinition, Delegatable):
40
39
  @staticmethod
41
40
  def load(data, play, current_role_path=None, parent_role=None, variable_manager=None, loader=None, collection_list=None):
42
41
 
43
- if not (isinstance(data, string_types) or isinstance(data, dict)):
42
+ if not (isinstance(data, str) or isinstance(data, dict)):
44
43
  raise AnsibleParserError("Invalid role definition.", obj=data)
45
44
 
46
- if isinstance(data, string_types) and ',' in data:
45
+ if isinstance(data, str) and ',' in data:
47
46
  raise AnsibleError("Invalid old style role requirement: %s" % data)
48
47
 
49
48
  ri = RoleInclude(play=play, role_basedir=current_role_path, variable_manager=variable_manager, loader=loader, collection_list=collection_list)
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
  import os
21
21
 
22
22
  from ansible.errors import AnsibleParserError, AnsibleError
23
- from ansible.module_utils.six import string_types
24
23
  from ansible.playbook.attribute import NonInheritableFieldAttribute
25
24
  from ansible.playbook.base import Base
26
25
  from ansible.playbook.collectionsearch import CollectionSearch
@@ -70,7 +69,7 @@ class RoleMetadata(Base, CollectionSearch):
70
69
 
71
70
  for role_def in ds:
72
71
  # FIXME: consolidate with ansible-galaxy to keep this in sync
73
- if isinstance(role_def, string_types) or 'role' in role_def or 'name' in role_def:
72
+ if isinstance(role_def, str) or 'role' in role_def or 'name' in role_def:
74
73
  roles.append(role_def)
75
74
  continue
76
75
  try:
@@ -106,13 +105,3 @@ class RoleMetadata(Base, CollectionSearch):
106
105
  collection_search_list=collection_search_list)
107
106
  except AssertionError as ex:
108
107
  raise AnsibleParserError("A malformed list of role dependencies was encountered.", obj=self._ds) from ex
109
-
110
- def serialize(self):
111
- return dict(
112
- allow_duplicates=self._allow_duplicates,
113
- dependencies=self._dependencies
114
- )
115
-
116
- def deserialize(self, data):
117
- setattr(self, 'allow_duplicates', data.get('allow_duplicates', False))
118
- setattr(self, 'dependencies', data.get('dependencies', []))
@@ -18,7 +18,6 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  from ansible.errors import AnsibleError
21
- from ansible.module_utils.six import string_types
22
21
  from ansible.playbook.role.definition import RoleDefinition
23
22
  from ansible.utils.display import Display
24
23
  from ansible.utils.galaxy import scm_archive_resource
@@ -65,7 +64,7 @@ class RoleRequirement(RoleDefinition):
65
64
  @staticmethod
66
65
  def role_yaml_parse(role):
67
66
 
68
- if isinstance(role, string_types):
67
+ if isinstance(role, str):
69
68
  name = None
70
69
  scm = None
71
70
  src = None
@@ -23,7 +23,6 @@ from ansible.playbook.task_include import TaskInclude
23
23
  from ansible.playbook.role import Role
24
24
  from ansible.playbook.role.include import RoleInclude
25
25
  from ansible.utils.display import Display
26
- from ansible.module_utils.six import string_types
27
26
  from ansible._internal._templating._engine import TemplateEngine
28
27
 
29
28
  __all__ = ['IncludeRole']
@@ -137,7 +136,7 @@ class IncludeRole(TaskInclude):
137
136
  for key in my_arg_names.intersection(IncludeRole.FROM_ARGS):
138
137
  from_key = key.removesuffix('_from')
139
138
  args_value = ir.args.get(key)
140
- if not isinstance(args_value, string_types):
139
+ if not isinstance(args_value, str):
141
140
  raise AnsibleParserError('Expected a string for %s but got %s instead' % (key, type(args_value)))
142
141
  ir._from_files[from_key] = args_value
143
142
 
@@ -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],