ansible-core 2.19.4rc1__py3-none-any.whl → 2.20.0__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 (215) 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 +67 -67
  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/config/base.yml +1 -25
  19. ansible/config/manager.py +0 -2
  20. ansible/executor/play_iterator.py +42 -20
  21. ansible/executor/playbook_executor.py +0 -9
  22. ansible/executor/task_executor.py +26 -18
  23. ansible/executor/task_queue_manager.py +1 -3
  24. ansible/galaxy/api.py +33 -80
  25. ansible/galaxy/collection/__init__.py +11 -21
  26. ansible/galaxy/dependency_resolution/__init__.py +10 -9
  27. ansible/galaxy/dependency_resolution/dataclasses.py +86 -70
  28. ansible/galaxy/dependency_resolution/providers.py +54 -134
  29. ansible/galaxy/dependency_resolution/versioning.py +2 -4
  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 +26 -23
  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/yumdnf.py +0 -5
  58. ansible/modules/apt.py +18 -13
  59. ansible/modules/apt_repository.py +1 -1
  60. ansible/modules/assemble.py +5 -9
  61. ansible/modules/blockinfile.py +39 -23
  62. ansible/modules/cron.py +26 -35
  63. ansible/modules/deb822_repository.py +83 -12
  64. ansible/modules/dnf.py +3 -7
  65. ansible/modules/dnf5.py +4 -6
  66. ansible/modules/expect.py +0 -3
  67. ansible/modules/find.py +1 -2
  68. ansible/modules/get_url.py +1 -1
  69. ansible/modules/git.py +4 -5
  70. ansible/modules/include_vars.py +1 -1
  71. ansible/modules/known_hosts.py +7 -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/vault/__init__.py +1 -2
  85. ansible/playbook/base.py +8 -56
  86. ansible/playbook/block.py +0 -60
  87. ansible/playbook/collectionsearch.py +1 -2
  88. ansible/playbook/handler.py +1 -7
  89. ansible/playbook/helpers.py +0 -7
  90. ansible/playbook/included_file.py +1 -1
  91. ansible/playbook/play.py +102 -36
  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 +11 -50
  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 +5 -2
  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.4rc1.dist-info → ansible_core-2.20.0.dist-info}/METADATA +4 -4
  153. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/RECORD +213 -214
  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/ansible.txt +1 -1
  159. ansible_test/_data/requirements/sanity.ansible-doc.txt +2 -2
  160. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  161. ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
  162. ansible_test/_data/requirements/sanity.import.txt +1 -1
  163. ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
  164. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  165. ansible_test/_data/requirements/sanity.pylint.txt +6 -6
  166. ansible_test/_data/requirements/sanity.runtime-metadata.txt +1 -1
  167. ansible_test/_data/requirements/sanity.validate-modules.txt +2 -2
  168. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  169. ansible_test/_internal/cache.py +2 -5
  170. ansible_test/_internal/cli/compat.py +1 -1
  171. ansible_test/_internal/commands/coverage/combine.py +1 -3
  172. ansible_test/_internal/commands/integration/__init__.py +3 -7
  173. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  174. ansible_test/_internal/commands/integration/coverage.py +1 -3
  175. ansible_test/_internal/commands/integration/filters.py +5 -10
  176. ansible_test/_internal/commands/sanity/pylint.py +11 -0
  177. ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
  178. ansible_test/_internal/commands/units/__init__.py +1 -13
  179. ansible_test/_internal/compat/packaging.py +2 -2
  180. ansible_test/_internal/compat/yaml.py +2 -2
  181. ansible_test/_internal/completion.py +2 -5
  182. ansible_test/_internal/config.py +2 -7
  183. ansible_test/_internal/coverage_util.py +1 -1
  184. ansible_test/_internal/delegation.py +2 -0
  185. ansible_test/_internal/docker_util.py +1 -1
  186. ansible_test/_internal/host_profiles.py +6 -11
  187. ansible_test/_internal/provider/__init__.py +2 -5
  188. ansible_test/_internal/provisioning.py +2 -5
  189. ansible_test/_internal/pypi_proxy.py +1 -1
  190. ansible_test/_internal/python_requirements.py +1 -1
  191. ansible_test/_internal/target.py +2 -6
  192. ansible_test/_internal/thread.py +1 -4
  193. ansible_test/_internal/util.py +9 -14
  194. ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
  195. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +48 -45
  196. ansible_test/_util/controller/sanity/pylint/plugins/string_format.py +9 -7
  197. ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +51 -37
  198. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
  199. ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
  200. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
  201. ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
  202. ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
  203. ansible_test/_util/target/common/constants.py +2 -2
  204. ansible_test/_util/target/setup/bootstrap.sh +0 -6
  205. ansible/utils/py3compat.py +0 -27
  206. ansible_test/_data/pytest/config/legacy.ini +0 -4
  207. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/WHEEL +0 -0
  208. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/entry_points.txt +0 -0
  209. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/COPYING +0 -0
  210. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  211. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  212. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  213. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  214. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  215. {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@
2
2
  # Copyright: (c) 2017, Ansible Project
3
3
  # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
4
 
5
- from __future__ import annotations
6
5
  from __future__ import annotations
7
6
 
8
7
  import copy
@@ -19,7 +18,6 @@ from ansible._internal._errors import _error_utils
19
18
  from ansible.module_utils.basic import is_executable
20
19
  from ansible._internal._datatag._tags import Origin, TrustedAsTemplate, SourceWasEncrypted
21
20
  from ansible.module_utils._internal._datatag import AnsibleTagHelper
22
- from ansible.module_utils.six import binary_type, text_type
23
21
  from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
24
22
  from ansible.parsing.quoting import unquote
25
23
  from ansible.parsing.utils.yaml import from_yaml
@@ -32,7 +30,7 @@ display = Display()
32
30
 
33
31
  # Tries to determine if a path is inside a role, last dir must be 'tasks'
34
32
  # this is not perfect but people should really avoid 'tasks' dirs outside roles when using Ansible.
35
- RE_TASKS = re.compile(u'(?:^|%s)+tasks%s?$' % (os.path.sep, os.path.sep))
33
+ RE_TASKS = re.compile('(?:^|%s)+tasks%s?$' % (os.path.sep, os.path.sep))
36
34
 
37
35
 
38
36
  class DataLoader:
@@ -49,28 +47,27 @@ class DataLoader:
49
47
  Usage:
50
48
 
51
49
  dl = DataLoader()
52
- # optionally: dl.set_vault_secrets([('default', ansible.parsing.vault.PrompVaultSecret(...),)])
50
+ # optionally: dl.set_vault_secrets([('default', ansible.parsing.vault.PromptVaultSecret(...),)])
53
51
  ds = dl.load('...')
54
52
  ds = dl.load_from_file('/path/to/file')
55
53
  """
56
54
 
57
- def __init__(self):
55
+ def __init__(self) -> None:
58
56
 
59
- self._basedir = '.'
57
+ self._basedir: str = os.path.abspath('.')
60
58
 
61
59
  # NOTE: not effective with forks as the main copy does not get updated.
62
60
  # avoids rereading files
63
- self._FILE_CACHE = dict()
61
+ self._FILE_CACHE: dict[str, object] = {}
64
62
 
65
63
  # NOTE: not thread safe, also issues with forks not returning data to main proc
66
64
  # so they need to be cleaned independently. See WorkerProcess for example.
67
65
  # used to keep track of temp files for cleaning
68
- self._tempfiles = set()
66
+ self._tempfiles: set[str] = set()
69
67
 
70
68
  # initialize the vault stuff with an empty password
71
69
  # TODO: replace with a ref to something that can get the password
72
70
  # a creds/auth provider
73
- self._vaults = {}
74
71
  self._vault = VaultLib()
75
72
  self.set_vault_secrets(None)
76
73
 
@@ -230,23 +227,19 @@ class DataLoader:
230
227
 
231
228
  def set_basedir(self, basedir: str) -> None:
232
229
  """ sets the base directory, used to find files when a relative path is given """
233
-
234
- if basedir is not None:
235
- self._basedir = to_text(basedir)
230
+ self._basedir = os.path.abspath(basedir)
236
231
 
237
232
  def path_dwim(self, given: str) -> str:
238
233
  """
239
234
  make relative paths work like folks expect.
240
235
  """
241
236
 
242
- given = to_text(given, errors='surrogate_or_strict')
243
237
  given = unquote(given)
244
238
 
245
- if given.startswith(to_text(os.path.sep)) or given.startswith(u'~'):
239
+ if given.startswith(os.path.sep) or given.startswith('~'):
246
240
  path = given
247
241
  else:
248
- basedir = to_text(self._basedir, errors='surrogate_or_strict')
249
- path = os.path.join(basedir, given)
242
+ path = os.path.join(self._basedir, given)
250
243
 
251
244
  return unfrackpath(path, follow=False)
252
245
 
@@ -294,10 +287,9 @@ class DataLoader:
294
287
  """
295
288
 
296
289
  search = []
297
- source = to_text(source, errors='surrogate_or_strict')
298
290
 
299
291
  # I have full path, nothing else needs to be looked at
300
- if source.startswith(to_text(os.path.sep)) or source.startswith(u'~'):
292
+ if source.startswith(os.path.sep) or source.startswith('~'):
301
293
  search.append(unfrackpath(source, follow=False))
302
294
  else:
303
295
  # base role/play path + templates/files/vars + relative filename
@@ -364,7 +356,7 @@ class DataLoader:
364
356
  if os.path.exists(to_bytes(test_path, errors='surrogate_or_strict')):
365
357
  result = test_path
366
358
  else:
367
- display.debug(u'evaluation_path:\n\t%s' % '\n\t'.join(paths))
359
+ display.debug('evaluation_path:\n\t%s' % '\n\t'.join(paths))
368
360
  for path in paths:
369
361
  upath = unfrackpath(path, follow=False)
370
362
  b_upath = to_bytes(upath, errors='surrogate_or_strict')
@@ -385,9 +377,9 @@ class DataLoader:
385
377
  search.append(os.path.join(to_bytes(self.get_basedir(), errors='surrogate_or_strict'), b_dirname, b_source))
386
378
  search.append(os.path.join(to_bytes(self.get_basedir(), errors='surrogate_or_strict'), b_source))
387
379
 
388
- display.debug(u'search_path:\n\t%s' % to_text(b'\n\t'.join(search)))
380
+ display.debug('search_path:\n\t%s' % to_text(b'\n\t'.join(search)))
389
381
  for b_candidate in search:
390
- display.vvvvv(u'looking for "%s" at "%s"' % (source, to_text(b_candidate)))
382
+ display.vvvvv('looking for "%s" at "%s"' % (source, to_text(b_candidate)))
391
383
  if os.path.exists(b_candidate):
392
384
  result = to_text(b_candidate)
393
385
  break
@@ -418,11 +410,10 @@ class DataLoader:
418
410
  Temporary files are cleanup in the destructor
419
411
  """
420
412
 
421
- if not file_path or not isinstance(file_path, (binary_type, text_type)):
413
+ if not file_path or not isinstance(file_path, (bytes, str)):
422
414
  raise AnsibleParserError("Invalid filename: '%s'" % to_native(file_path))
423
415
 
424
- b_file_path = to_bytes(file_path, errors='surrogate_or_strict')
425
- if not self.path_exists(b_file_path) or not self.is_file(b_file_path):
416
+ if not self.path_exists(file_path) or not self.is_file(file_path):
426
417
  raise AnsibleFileNotFound(file_name=file_path)
427
418
 
428
419
  real_path = self.path_dwim(file_path)
@@ -480,7 +471,7 @@ class DataLoader:
480
471
  """
481
472
 
482
473
  b_path = to_bytes(os.path.join(path, name))
483
- found = []
474
+ found: list[str] = []
484
475
 
485
476
  if extensions is None:
486
477
  # Look for file with no extension first to find dir before file
@@ -489,27 +480,29 @@ class DataLoader:
489
480
  for ext in extensions:
490
481
 
491
482
  if '.' in ext:
492
- full_path = b_path + to_bytes(ext)
483
+ b_full_path = b_path + to_bytes(ext)
493
484
  elif ext:
494
- full_path = b'.'.join([b_path, to_bytes(ext)])
485
+ b_full_path = b'.'.join([b_path, to_bytes(ext)])
495
486
  else:
496
- full_path = b_path
487
+ b_full_path = b_path
488
+
489
+ full_path = to_text(b_full_path)
497
490
 
498
491
  if self.path_exists(full_path):
499
492
  if self.is_directory(full_path):
500
493
  if allow_dir:
501
- found.extend(self._get_dir_vars_files(to_text(full_path), extensions))
494
+ found.extend(self._get_dir_vars_files(full_path, extensions))
502
495
  else:
503
496
  continue
504
497
  else:
505
- found.append(to_text(full_path))
498
+ found.append(full_path)
506
499
  break
507
500
  return found
508
501
 
509
502
  def _get_dir_vars_files(self, path: str, extensions: list[str]) -> list[str]:
510
503
  found = []
511
504
  for spath in sorted(self.list_directory(path)):
512
- if not spath.startswith(u'.') and not spath.endswith(u'~'): # skip hidden and backups
505
+ if not spath.startswith('.') and not spath.endswith('~'): # skip hidden and backups
513
506
 
514
507
  ext = os.path.splitext(spath)[-1]
515
508
  full_spath = os.path.join(path, spath)
@@ -59,7 +59,6 @@ except ImportError:
59
59
 
60
60
  from ansible.errors import AnsibleError, AnsibleAssertionError
61
61
  from ansible import constants as C
62
- from ansible.module_utils.six import binary_type
63
62
  from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
64
63
  from ansible.utils.display import Display
65
64
  from ansible.utils.path import makedirs_safe, unfrackpath
@@ -1237,7 +1236,7 @@ class VaultAES256:
1237
1236
 
1238
1237
  It would be nice if there were a library for this but hey.
1239
1238
  """
1240
- if not (isinstance(b_a, binary_type) and isinstance(b_b, binary_type)):
1239
+ if not (isinstance(b_a, bytes) and isinstance(b_b, bytes)):
1241
1240
  raise TypeError('_is_equal can only be used to compare two byte strings')
1242
1241
 
1243
1242
  # http://codahale.com/a-lesson-in-timing-attacks/
ansible/playbook/base.py CHANGED
@@ -19,7 +19,6 @@ from ansible import context
19
19
  from ansible.errors import AnsibleError, AnsibleParserError, AnsibleAssertionError, AnsibleValueOmittedError, AnsibleFieldAttributeError
20
20
  from ansible.module_utils.datatag import native_type_name
21
21
  from ansible._internal._datatag._tags import Origin
22
- from ansible.module_utils.six import string_types
23
22
  from ansible.module_utils.parsing.convert_bool import boolean
24
23
  from ansible.module_utils.common.sentinel import Sentinel
25
24
  from ansible.module_utils.common.text.converters import to_text
@@ -37,7 +36,7 @@ display = Display()
37
36
  def _validate_action_group_metadata(action, found_group_metadata, fq_group_name):
38
37
  valid_metadata = {
39
38
  'extend_group': {
40
- 'types': (list, string_types,),
39
+ 'types': (list, str,),
41
40
  'errortype': 'list',
42
41
  },
43
42
  }
@@ -204,7 +203,7 @@ class FieldAttributeBase:
204
203
  value = self.set_to_context(attr.name)
205
204
 
206
205
  valid_values = frozenset(('always', 'on_failed', 'on_unreachable', 'on_skipped', 'never'))
207
- if value and isinstance(value, string_types) and value not in valid_values:
206
+ if value and isinstance(value, str) and value not in valid_values:
208
207
  raise AnsibleParserError("'%s' is not a valid value for debugger. Must be one of %s" % (value, ', '.join(valid_values)), obj=self.get_ds())
209
208
  return value
210
209
 
@@ -350,14 +349,14 @@ class FieldAttributeBase:
350
349
  found_group_metadata = False
351
350
  for action in action_group:
352
351
  # Everything should be a string except the metadata entry
353
- if not isinstance(action, string_types):
352
+ if not isinstance(action, str):
354
353
  _validate_action_group_metadata(action, found_group_metadata, fq_group_name)
355
354
 
356
355
  if isinstance(action['metadata'], dict):
357
356
  found_group_metadata = True
358
357
 
359
358
  include_groups = action['metadata'].get('extend_group', [])
360
- if isinstance(include_groups, string_types):
359
+ if isinstance(include_groups, str):
361
360
  include_groups = [include_groups]
362
361
  if not isinstance(include_groups, list):
363
362
  # Bad entries may be a warning above, but prevent tracebacks by setting it back to the acceptable type.
@@ -472,7 +471,7 @@ class FieldAttributeBase:
472
471
  elif attribute.isa == 'percent':
473
472
  # special value, which may be an integer or float
474
473
  # with an optional '%' at the end
475
- if isinstance(value, string_types) and '%' in value:
474
+ if isinstance(value, str) and '%' in value:
476
475
  value = value.replace('%', '')
477
476
  value = float(value)
478
477
  elif attribute.isa == 'list':
@@ -660,8 +659,8 @@ class FieldAttributeBase:
660
659
  attrs = {}
661
660
  for (name, attribute) in self.fattributes.items():
662
661
  attr = getattr(self, name)
663
- if attribute.isa == 'class' and hasattr(attr, 'serialize'):
664
- attrs[name] = attr.serialize()
662
+ if attribute.isa == 'class':
663
+ attrs[name] = attr.dump_attrs()
665
664
  else:
666
665
  attrs[name] = attr
667
666
  return attrs
@@ -675,60 +674,13 @@ class FieldAttributeBase:
675
674
  attribute = self.fattributes[attr]
676
675
  if attribute.isa == 'class' and isinstance(value, dict):
677
676
  obj = attribute.class_type()
678
- obj.deserialize(value)
677
+ obj.from_attrs(value)
679
678
  setattr(self, attr, obj)
680
679
  else:
681
680
  setattr(self, attr, value)
682
681
  else:
683
682
  setattr(self, attr, value) # overridden dump_attrs in derived types may dump attributes which are not field attributes
684
683
 
685
- # from_attrs is only used to create a finalized task
686
- # from attrs from the Worker/TaskExecutor
687
- # Those attrs are finalized and squashed in the TE
688
- # and controller side use needs to reflect that
689
- self._finalized = True
690
- self._squashed = True
691
-
692
- def serialize(self):
693
- """
694
- Serializes the object derived from the base object into
695
- a dictionary of values. This only serializes the field
696
- attributes for the object, so this may need to be overridden
697
- for any classes which wish to add additional items not stored
698
- as field attributes.
699
- """
700
-
701
- repr = self.dump_attrs()
702
-
703
- # serialize the uuid field
704
- repr['uuid'] = self._uuid
705
- repr['finalized'] = self._finalized
706
- repr['squashed'] = self._squashed
707
-
708
- return repr
709
-
710
- def deserialize(self, data):
711
- """
712
- Given a dictionary of values, load up the field attributes for
713
- this object. As with serialize(), if there are any non-field
714
- attribute data members, this method will need to be overridden
715
- and extended.
716
- """
717
-
718
- if not isinstance(data, dict):
719
- raise AnsibleAssertionError('data (%s) should be a dict but is a %s' % (data, type(data)))
720
-
721
- for (name, attribute) in self.fattributes.items():
722
- if name in data:
723
- setattr(self, name, data[name])
724
- else:
725
- self.set_to_context(name)
726
-
727
- # restore the UUID field
728
- setattr(self, '_uuid', data.get('uuid'))
729
- self._finalized = data.get('finalized', False)
730
- self._squashed = data.get('squashed', False)
731
-
732
684
 
733
685
  class Base(FieldAttributeBase):
734
686
 
ansible/playbook/block.py CHANGED
@@ -26,7 +26,6 @@ from ansible.playbook.collectionsearch import CollectionSearch
26
26
  from ansible.playbook.delegatable import Delegatable
27
27
  from ansible.playbook.helpers import load_list_of_tasks
28
28
  from ansible.playbook.notifiable import Notifiable
29
- from ansible.playbook.role import Role
30
29
  from ansible.playbook.taggable import Taggable
31
30
 
32
31
 
@@ -219,65 +218,6 @@ class Block(Base, Conditional, CollectionSearch, Taggable, Notifiable, Delegatab
219
218
  new_me.validate()
220
219
  return new_me
221
220
 
222
- def serialize(self):
223
- """
224
- Override of the default serialize method, since when we're serializing
225
- a task we don't want to include the attribute list of tasks.
226
- """
227
-
228
- data = dict()
229
- for attr in self.fattributes:
230
- if attr not in ('block', 'rescue', 'always'):
231
- data[attr] = getattr(self, attr)
232
-
233
- data['dep_chain'] = self.get_dep_chain()
234
-
235
- if self._role is not None:
236
- data['role'] = self._role.serialize()
237
- if self._parent is not None:
238
- data['parent'] = self._parent.copy(exclude_tasks=True).serialize()
239
- data['parent_type'] = self._parent.__class__.__name__
240
-
241
- return data
242
-
243
- def deserialize(self, data):
244
- """
245
- Override of the default deserialize method, to match the above overridden
246
- serialize method
247
- """
248
-
249
- # import is here to avoid import loops
250
- from ansible.playbook.task_include import TaskInclude
251
- from ansible.playbook.handler_task_include import HandlerTaskInclude
252
-
253
- # we don't want the full set of attributes (the task lists), as that
254
- # would lead to a serialize/deserialize loop
255
- for attr in self.fattributes:
256
- if attr in data and attr not in ('block', 'rescue', 'always'):
257
- setattr(self, attr, data.get(attr))
258
-
259
- self._dep_chain = data.get('dep_chain', None)
260
-
261
- # if there was a serialized role, unpack it too
262
- role_data = data.get('role')
263
- if role_data:
264
- r = Role()
265
- r.deserialize(role_data)
266
- self._role = r
267
-
268
- parent_data = data.get('parent')
269
- if parent_data:
270
- parent_type = data.get('parent_type')
271
- if parent_type == 'Block':
272
- p = Block()
273
- elif parent_type == 'TaskInclude':
274
- p = TaskInclude()
275
- elif parent_type == 'HandlerTaskInclude':
276
- p = HandlerTaskInclude()
277
- p.deserialize(parent_data)
278
- self._parent = p
279
- self._dep_chain = self._parent.get_dep_chain()
280
-
281
221
  def set_loader(self, loader):
282
222
  self._loader = loader
283
223
  if self._parent:
@@ -3,7 +3,6 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from ansible.module_utils.six import string_types
7
6
  from ansible.playbook.attribute import FieldAttribute
8
7
  from ansible.utils.collection_loader import AnsibleCollectionConfig
9
8
  from ansible.utils.display import Display
@@ -32,7 +31,7 @@ def _ensure_default_collection(collection_list=None):
32
31
  class CollectionSearch:
33
32
 
34
33
  # this needs to be populated before we can resolve tasks/roles/etc
35
- collections = FieldAttribute(isa='list', listof=string_types, priority=100, default=_ensure_default_collection, always_post_validate=True, static=True)
34
+ collections = FieldAttribute(isa='list', listof=(str,), priority=100, default=_ensure_default_collection, always_post_validate=True, static=True)
36
35
 
37
36
  def _load_collections(self, attr, ds):
38
37
  # We are always a mixin with Base, so we can validate this untemplated
@@ -20,12 +20,11 @@ from __future__ import annotations
20
20
  from ansible.errors import AnsibleAssertionError
21
21
  from ansible.playbook.attribute import NonInheritableFieldAttribute
22
22
  from ansible.playbook.task import Task
23
- from ansible.module_utils.six import string_types
24
23
 
25
24
 
26
25
  class Handler(Task):
27
26
 
28
- listen = NonInheritableFieldAttribute(isa='list', default=list, listof=string_types, static=True)
27
+ listen = NonInheritableFieldAttribute(isa='list', default=list, listof=(str,), static=True)
29
28
 
30
29
  def __init__(self, block=None, role=None, task_include=None):
31
30
  self.notified_hosts = []
@@ -72,8 +71,3 @@ class Handler(Task):
72
71
 
73
72
  def is_host_notified(self, host):
74
73
  return host in self.notified_hosts
75
-
76
- def serialize(self):
77
- result = super(Handler, self).serialize()
78
- result['is_handler'] = True
79
- return result
@@ -232,13 +232,6 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
232
232
  variable_manager=variable_manager,
233
233
  )
234
234
 
235
- tags = ti_copy.tags[:]
236
-
237
- # now we extend the tags on each of the included blocks
238
- for b in included_blocks:
239
- b.tags = list(set(b.tags).union(tags))
240
- # FIXME - END
241
-
242
235
  # FIXME: handlers shouldn't need this special handling, but do
243
236
  # right now because they don't iterate blocks correctly
244
237
  if use_handlers:
@@ -203,7 +203,7 @@ class IncludedFile:
203
203
  for from_arg in new_task.FROM_ARGS:
204
204
  if from_arg in include_args:
205
205
  from_key = from_arg.removesuffix('_from')
206
- new_task._from_files[from_key] = include_args.pop(from_arg)
206
+ new_task._from_files[from_key] = include_args.get(from_arg)
207
207
 
208
208
  inc_file = IncludedFile(role_name, include_args, special_vars, new_task, is_role=True)
209
209
 
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):
@@ -390,36 +397,6 @@ class Play(Base, Taggable, CollectionSearch):
390
397
  tasklist.append(task)
391
398
  return tasklist
392
399
 
393
- def serialize(self):
394
- data = super(Play, self).serialize()
395
-
396
- roles = []
397
- for role in self.get_roles():
398
- roles.append(role.serialize())
399
- data['roles'] = roles
400
- data['included_path'] = self._included_path
401
- data['action_groups'] = self._action_groups
402
- data['group_actions'] = self._group_actions
403
-
404
- return data
405
-
406
- def deserialize(self, data):
407
- super(Play, self).deserialize(data)
408
-
409
- self._included_path = data.get('included_path', None)
410
- self._action_groups = data.get('action_groups', {})
411
- self._group_actions = data.get('group_actions', {})
412
- if 'roles' in data:
413
- role_data = data.get('roles', [])
414
- roles = []
415
- for role in role_data:
416
- r = Role()
417
- r.deserialize(role)
418
- roles.append(r)
419
-
420
- setattr(self, 'roles', roles)
421
- del data['roles']
422
-
423
400
  def copy(self):
424
401
  new_me = super(Play, self).copy()
425
402
  new_me.role_cache = self.role_cache.copy()
@@ -428,3 +405,92 @@ class Play(Base, Taggable, CollectionSearch):
428
405
  new_me._action_groups = self._action_groups
429
406
  new_me._group_actions = self._group_actions
430
407
  return new_me
408
+
409
+ def _post_validate_validate_argspec(self, attr: NonInheritableFieldAttribute, value: object, templar: _TE) -> str | None:
410
+ """Validate user input is a bool or string, and return the corresponding argument spec name."""
411
+
412
+ # Ensure the configuration is valid
413
+ if isinstance(value, str):
414
+ try:
415
+ value = templar.template(value)
416
+ except AnsibleValueOmittedError:
417
+ value = False
418
+
419
+ if not isinstance(value, (str, bool)):
420
+ raise AnsibleParserError(f"validate_argspec must be a boolean or string, not {type(value)}", obj=value)
421
+
422
+ # Short-circuit if configuration is turned off or inapplicable
423
+ if not value or self._origin is None:
424
+ return None
425
+
426
+ # Use the requested argument spec or fall back to the play name
427
+ argspec_name = None
428
+ if isinstance(value, str):
429
+ argspec_name = value
430
+ elif self._ds.get("name"):
431
+ argspec_name = self.name
432
+
433
+ metadata_err = argspec_err = ""
434
+ if not argspec_name:
435
+ argspec_err = (
436
+ "A play name is required when validate_argspec is True. "
437
+ "Alternatively, set validate_argspec to the name of an argument spec."
438
+ )
439
+ if self._metadata_path is None:
440
+ metadata_err = "A playbook meta file is required. Considered:\n - "
441
+ metadata_err += "\n - ".join([path.as_posix() for path in self._metadata_candidate_paths])
442
+
443
+ if metadata_err or argspec_err:
444
+ error = f"{argspec_err + (' ' if argspec_err else '')}{metadata_err}"
445
+ raise AnsibleParserError(error, obj=self._origin)
446
+
447
+ metadata = self._loader.load_from_file(self._metadata_path)
448
+
449
+ try:
450
+ metadata = metadata['argument_specs']
451
+ metadata = metadata[argspec_name]
452
+ options = metadata['options']
453
+ except (TypeError, KeyError):
454
+ options = None
455
+
456
+ if not isinstance(options, dict):
457
+ raise AnsibleParserError(
458
+ f"No argument spec named '{argspec_name}' in {self._metadata_path}. Minimally expected:\n"
459
+ + yaml_dump({"argument_specs": {f"{argspec_name!s}": {"options": {}}}}),
460
+ obj=metadata,
461
+ )
462
+
463
+ return argspec_name
464
+
465
+ @property
466
+ def _metadata_candidate_paths(self) -> list[_pathlib.Path]:
467
+ """A list of possible playbook.meta paths in configured order."""
468
+ extensions = C.config.get_config_value("YAML_FILENAME_EXTENSIONS")
469
+ if self._origin.path.endswith(tuple(extensions)):
470
+ playbook_without_ext = self._origin.path.rsplit('.', 1)[0]
471
+ else:
472
+ playbook_without_ext = self._origin.path
473
+
474
+ return [_pathlib.Path(playbook_without_ext + ".meta" + ext) for ext in extensions + ['']]
475
+
476
+ @_functools.cached_property
477
+ def _metadata_path(self) -> str | None:
478
+ """Locate playbook meta path:
479
+
480
+ playbook{ext?} -> playbook.meta{ext?}
481
+ """
482
+ if self._origin is None:
483
+ # adhoc, ansible-console don't have an associated playbook
484
+ return None
485
+ for candidate in self._metadata_candidate_paths:
486
+ if candidate.is_file():
487
+ return candidate.as_posix()
488
+ return None
489
+
490
+ @property
491
+ def argument_spec(self) -> dict:
492
+ """Retrieve the argument spec if one is configured."""
493
+ if not self.validate_argspec:
494
+ return {}
495
+
496
+ 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)