ansible-core 2.19.0b5__py3-none-any.whl → 2.19.0b7__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.
Files changed (184) hide show
  1. ansible/_internal/_ansiballz/__init__.py +0 -0
  2. ansible/_internal/_ansiballz/_builder.py +101 -0
  3. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  4. ansible/_internal/_templating/_jinja_bits.py +22 -4
  5. ansible/_internal/_templating/_jinja_common.py +1 -1
  6. ansible/_internal/_templating/_jinja_plugins.py +5 -2
  7. ansible/_internal/_templating/_template_vars.py +72 -0
  8. ansible/_internal/_templating/_transform.py +6 -0
  9. ansible/_internal/_yaml/_constructor.py +4 -4
  10. ansible/_internal/_yaml/_dumper.py +26 -18
  11. ansible/cli/__init__.py +9 -14
  12. ansible/cli/adhoc.py +6 -3
  13. ansible/cli/arguments/option_helpers.py +1 -1
  14. ansible/cli/console.py +2 -2
  15. ansible/cli/doc.py +4 -4
  16. ansible/cli/inventory.py +5 -7
  17. ansible/config/base.yml +33 -6
  18. ansible/errors/__init__.py +2 -1
  19. ansible/executor/module_common.py +75 -44
  20. ansible/executor/powershell/psrp_put_file.ps1 +1 -1
  21. ansible/executor/process/worker.py +2 -2
  22. ansible/executor/task_executor.py +2 -2
  23. ansible/executor/task_queue_manager.py +34 -70
  24. ansible/executor/task_result.py +1 -1
  25. ansible/galaxy/api.py +3 -6
  26. ansible/galaxy/collection/__init__.py +1 -6
  27. ansible/galaxy/collection/concrete_artifact_manager.py +4 -10
  28. ansible/galaxy/dependency_resolution/providers.py +3 -3
  29. ansible/galaxy/role.py +2 -2
  30. ansible/inventory/group.py +6 -1
  31. ansible/inventory/host.py +6 -1
  32. ansible/module_utils/_internal/__init__.py +7 -4
  33. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  34. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  35. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  36. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  37. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +10 -38
  38. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  39. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  40. ansible/module_utils/_internal/_datatag/__init__.py +23 -1
  41. ansible/module_utils/_internal/_deprecator.py +39 -34
  42. ansible/module_utils/_internal/_json/_profiles/__init__.py +1 -0
  43. ansible/module_utils/_internal/_messages.py +26 -2
  44. ansible/module_utils/_internal/_plugin_info.py +14 -1
  45. ansible/module_utils/ansible_release.py +1 -1
  46. ansible/module_utils/basic.py +58 -70
  47. ansible/module_utils/common/respawn.py +4 -41
  48. ansible/module_utils/common/yaml.py +1 -1
  49. ansible/module_utils/connection.py +8 -11
  50. ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
  51. ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
  52. ansible/module_utils/facts/hardware/base.py +1 -1
  53. ansible/module_utils/facts/hardware/linux.py +1 -1
  54. ansible/module_utils/facts/other/facter.py +1 -1
  55. ansible/module_utils/facts/sysctl.py +4 -6
  56. ansible/module_utils/facts/system/caps.py +2 -2
  57. ansible/module_utils/facts/system/distribution.py +2 -2
  58. ansible/module_utils/facts/system/local.py +1 -1
  59. ansible/module_utils/facts/virtual/linux.py +1 -1
  60. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  61. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  62. ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
  63. ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
  64. ansible/module_utils/service.py +1 -1
  65. ansible/module_utils/urls.py +5 -5
  66. ansible/modules/apt.py +9 -3
  67. ansible/modules/apt_repository.py +10 -10
  68. ansible/modules/assemble.py +7 -5
  69. ansible/modules/async_wrapper.py +7 -17
  70. ansible/modules/command.py +3 -3
  71. ansible/modules/copy.py +4 -4
  72. ansible/modules/cron.py +1 -1
  73. ansible/modules/expect.py +5 -5
  74. ansible/modules/file.py +16 -17
  75. ansible/modules/find.py +3 -3
  76. ansible/modules/get_url.py +17 -0
  77. ansible/modules/git.py +9 -7
  78. ansible/modules/hostname.py +2 -2
  79. ansible/modules/known_hosts.py +12 -14
  80. ansible/modules/package.py +6 -0
  81. ansible/modules/pip.py +9 -11
  82. ansible/modules/raw.py +2 -2
  83. ansible/modules/replace.py +2 -2
  84. ansible/modules/slurp.py +10 -13
  85. ansible/modules/stat.py +6 -8
  86. ansible/modules/unarchive.py +6 -6
  87. ansible/modules/user.py +1 -1
  88. ansible/modules/wait_for.py +38 -33
  89. ansible/modules/yum_repository.py +4 -3
  90. ansible/parsing/dataloader.py +2 -2
  91. ansible/parsing/mod_args.py +38 -20
  92. ansible/parsing/vault/__init__.py +9 -13
  93. ansible/playbook/base.py +7 -4
  94. ansible/playbook/helpers.py +1 -1
  95. ansible/playbook/included_file.py +3 -1
  96. ansible/playbook/play_context.py +2 -0
  97. ansible/playbook/playbook_include.py +23 -56
  98. ansible/playbook/role/__init__.py +38 -21
  99. ansible/playbook/taggable.py +19 -5
  100. ansible/playbook/task.py +2 -0
  101. ansible/plugins/action/__init__.py +2 -2
  102. ansible/plugins/action/assemble.py +2 -1
  103. ansible/plugins/action/assert.py +2 -2
  104. ansible/plugins/action/fetch.py +3 -3
  105. ansible/plugins/action/script.py +5 -4
  106. ansible/plugins/action/template.py +9 -3
  107. ansible/plugins/cache/__init__.py +17 -19
  108. ansible/plugins/callback/__init__.py +77 -87
  109. ansible/plugins/callback/default.py +0 -3
  110. ansible/plugins/callback/junit.py +0 -6
  111. ansible/plugins/callback/tree.py +5 -5
  112. ansible/plugins/connection/local.py +4 -4
  113. ansible/plugins/connection/paramiko_ssh.py +5 -5
  114. ansible/plugins/connection/ssh.py +9 -7
  115. ansible/plugins/connection/winrm.py +1 -1
  116. ansible/plugins/filter/core.py +19 -21
  117. ansible/plugins/filter/encryption.py +10 -2
  118. ansible/plugins/filter/pow.yml +1 -1
  119. ansible/plugins/filter/root.yml +1 -1
  120. ansible/plugins/filter/strftime.yml +3 -3
  121. ansible/plugins/filter/to_uuid.yml +1 -1
  122. ansible/plugins/inventory/script.py +1 -1
  123. ansible/plugins/list.py +5 -4
  124. ansible/plugins/loader.py +5 -0
  125. ansible/plugins/lookup/password.py +4 -6
  126. ansible/plugins/lookup/template.py +9 -4
  127. ansible/plugins/shell/powershell.py +3 -2
  128. ansible/plugins/shell/sh.py +3 -2
  129. ansible/plugins/strategy/__init__.py +3 -3
  130. ansible/plugins/test/core.py +2 -2
  131. ansible/release.py +1 -1
  132. ansible/template/__init__.py +9 -53
  133. ansible/utils/collection_loader/_collection_finder.py +3 -3
  134. ansible/utils/display.py +38 -37
  135. ansible/utils/galaxy.py +2 -2
  136. ansible/utils/hashing.py +6 -7
  137. ansible/utils/path.py +6 -8
  138. ansible/utils/py3compat.py +2 -1
  139. ansible/utils/ssh_functions.py +3 -2
  140. ansible/utils/vars.py +4 -1
  141. ansible/vars/manager.py +6 -3
  142. ansible/vars/plugins.py +3 -3
  143. ansible/vars/reserved.py +6 -4
  144. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
  145. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +184 -173
  146. ansible_test/_internal/__init__.py +5 -0
  147. ansible_test/_internal/ansible_util.py +1 -1
  148. ansible_test/_internal/classification/python.py +6 -0
  149. ansible_test/_internal/cli/commands/__init__.py +0 -5
  150. ansible_test/_internal/cli/environments.py +51 -5
  151. ansible_test/_internal/commands/coverage/__init__.py +1 -1
  152. ansible_test/_internal/commands/integration/__init__.py +18 -5
  153. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  154. ansible_test/_internal/commands/integration/coverage.py +7 -2
  155. ansible_test/_internal/commands/sanity/__init__.py +3 -1
  156. ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
  157. ansible_test/_internal/commands/shell/__init__.py +43 -4
  158. ansible_test/_internal/commands/units/__init__.py +4 -1
  159. ansible_test/_internal/config.py +21 -13
  160. ansible_test/_internal/debugging.py +166 -0
  161. ansible_test/_internal/delegation.py +21 -13
  162. ansible_test/_internal/host_profiles.py +259 -16
  163. ansible_test/_internal/inventory.py +4 -0
  164. ansible_test/_internal/metadata.py +94 -4
  165. ansible_test/_internal/processes.py +80 -0
  166. ansible_test/_internal/provisioning.py +10 -4
  167. ansible_test/_internal/python_requirements.py +27 -0
  168. ansible_test/_internal/ssh.py +1 -5
  169. ansible_test/_internal/target.py +8 -0
  170. ansible_test/_internal/thread.py +2 -1
  171. ansible_test/_internal/timeout.py +1 -1
  172. ansible_test/_internal/util.py +20 -12
  173. ansible_test/_internal/util_common.py +13 -3
  174. ansible_test/_util/target/injector/python.py +8 -0
  175. ansible_test/_util/target/setup/requirements.py +3 -9
  176. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
  177. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
  178. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
  179. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  180. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  181. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  182. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  183. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  184. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -256,8 +256,8 @@ def main():
256
256
  try:
257
257
  with open(path, 'rb') as f:
258
258
  contents = to_text(f.read(), errors='surrogate_or_strict', encoding=encoding)
259
- except (OSError, IOError) as e:
260
- module.fail_json(msg='Unable to read the contents of %s: %s' % (path, to_text(e)))
259
+ except OSError as ex:
260
+ raise Exception(f"Unable to read the contents of {path!r}.") from ex
261
261
 
262
262
  pattern = u''
263
263
  if params['after'] and params['before']:
ansible/modules/slurp.py CHANGED
@@ -85,7 +85,6 @@ import base64
85
85
  import errno
86
86
 
87
87
  from ansible.module_utils.basic import AnsibleModule
88
- from ansible.module_utils.common.text.converters import to_native
89
88
 
90
89
 
91
90
  def main():
@@ -99,20 +98,18 @@ def main():
99
98
 
100
99
  try:
101
100
  with open(source, 'rb') as source_fh:
102
- source_content = source_fh.read()
103
- except (IOError, OSError) as e:
104
- if e.errno == errno.ENOENT:
105
- msg = "file not found: %s" % source
106
- elif e.errno == errno.EACCES:
107
- msg = "file is not readable: %s" % source
108
- elif e.errno == errno.EISDIR:
109
- msg = "source is a directory and must be a file: %s" % source
101
+ data = base64.b64encode(source_fh.read())
102
+ except OSError as ex:
103
+ if ex.errno == errno.ENOENT:
104
+ msg = f"File not found: {source}"
105
+ elif ex.errno == errno.EACCES:
106
+ msg = f"File is not readable: {source}"
107
+ elif ex.errno == errno.EISDIR:
108
+ msg = f"Source is a directory and must be a file: {source}"
110
109
  else:
111
- msg = "unable to slurp file: %s" % to_native(e, errors='surrogate_then_replace')
110
+ msg = "Unable to slurp file: {source}"
112
111
 
113
- module.fail_json(msg)
114
-
115
- data = base64.b64encode(source_content)
112
+ module.fail_json(msg, exception=ex)
116
113
 
117
114
  module.exit_json(content=data, source=source, encoding='base64')
118
115
 
ansible/modules/stat.py CHANGED
@@ -354,7 +354,6 @@ stat:
354
354
  version_added: 2.3
355
355
  """
356
356
 
357
- import errno
358
357
  import grp
359
358
  import os
360
359
  import pwd
@@ -409,7 +408,7 @@ def format_output(module, path, st):
409
408
  ('st_blksize', 'block_size'),
410
409
  ('st_rdev', 'device_type'),
411
410
  ('st_flags', 'flags'),
412
- # Some Berkley based
411
+ # Some Berkeley based
413
412
  ('st_gen', 'generation'),
414
413
  ('st_birthtime', 'birthtime'),
415
414
  # RISCOS
@@ -456,12 +455,11 @@ def main():
456
455
  st = os.stat(b_path)
457
456
  else:
458
457
  st = os.lstat(b_path)
459
- except OSError as e:
460
- if e.errno == errno.ENOENT:
461
- output = {'exists': False}
462
- module.exit_json(changed=False, stat=output)
463
-
464
- module.fail_json(msg=e.strerror)
458
+ except FileNotFoundError:
459
+ output = {'exists': False}
460
+ module.exit_json(changed=False, stat=output)
461
+ except OSError as ex:
462
+ module.fail_json(msg=ex.strerror, exception=ex)
465
463
 
466
464
  # process base results
467
465
  output = format_output(module, path, st)
@@ -1132,8 +1132,8 @@ def main():
1132
1132
  res_args['extract_results'] = handler.unarchive()
1133
1133
  if res_args['extract_results']['rc'] != 0:
1134
1134
  module.fail_json(msg="failed to unpack %s to %s" % (src, dest), **res_args)
1135
- except IOError:
1136
- module.fail_json(msg="failed to unpack %s to %s" % (src, dest), **res_args)
1135
+ except OSError as ex:
1136
+ module.fail_json(f"Failed to unpack {src!r} to {dest!r}.", exception=ex, **res_args)
1137
1137
  else:
1138
1138
  res_args['changed'] = True
1139
1139
 
@@ -1150,8 +1150,8 @@ def main():
1150
1150
 
1151
1151
  try:
1152
1152
  res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed'], expand=False)
1153
- except (IOError, OSError) as e:
1154
- module.fail_json(msg="Unexpected error when accessing exploded file: %s" % to_native(e), **res_args)
1153
+ except OSError as ex:
1154
+ module.fail_json("Unexpected error when accessing exploded file.", exception=ex, **res_args)
1155
1155
 
1156
1156
  if '/' in filename:
1157
1157
  top_folder_path = filename.split('/')[0]
@@ -1165,8 +1165,8 @@ def main():
1165
1165
  file_args['path'] = "%s/%s" % (dest, f)
1166
1166
  try:
1167
1167
  res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed'], expand=False)
1168
- except (IOError, OSError) as e:
1169
- module.fail_json(msg="Unexpected error when accessing exploded file: %s" % to_native(e), **res_args)
1168
+ except OSError as ex:
1169
+ module.fail_json("Unexpected error when accessing exploded file.", exception=ex, **res_args)
1170
1170
 
1171
1171
  if module.params['list_files']:
1172
1172
  res_args['files'] = handler.files_in_archive
ansible/modules/user.py CHANGED
@@ -1341,7 +1341,7 @@ class User(object):
1341
1341
  try:
1342
1342
  with open(ssh_public_key_file, 'r') as f:
1343
1343
  ssh_public_key = f.read().strip()
1344
- except IOError:
1344
+ except OSError:
1345
1345
  return None
1346
1346
  return ssh_public_key
1347
1347
 
@@ -76,6 +76,8 @@ options:
76
76
  description:
77
77
  - Can be used to match a string in either a file or a socket connection.
78
78
  - Defaults to a multiline regex.
79
+ - When inspecting a system log file and a static string, remember that Ansible by default logs its own actions there;
80
+ see the notes and examples for information.
79
81
  type: str
80
82
  version_added: "1.4"
81
83
  exclude_hosts:
@@ -105,13 +107,13 @@ attributes:
105
107
  platform:
106
108
  platforms: posix
107
109
  notes:
108
- - The ability to use search_regex with a port connection was added in Ansible 1.7.
109
- - Prior to Ansible 2.4, testing for the absence of a directory or UNIX socket did not work correctly.
110
- - Prior to Ansible 2.4, testing for the presence of a file did not work correctly if the remote user did not have read access to that file.
111
110
  - Under some circumstances when using mandatory access control, a path may always be treated as being absent even if it exists, but
112
111
  can't be modified or created by the remote user either.
113
112
  - When waiting for a path, symbolic links will be followed. Many other modules that manipulate files do not follow symbolic links,
114
113
  so operations on the path using other modules may not work exactly as expected.
114
+ - When searching a static string within a system log file, it is important to account for potential self-matching against log entries
115
+ generated by the Ansible modules. To prevent this, add a regular expression construct into the search string. For example, to match
116
+ a literal string 'this thing', one could use a regular expression like 'this t[h]ing'.
115
117
  seealso:
116
118
  - module: ansible.builtin.wait_for_connection
117
119
  - module: ansible.windows.win_wait_for
@@ -156,6 +158,11 @@ EXAMPLES = r"""
156
158
  path: /tmp/foo
157
159
  search_regex: completed
158
160
 
161
+ - name: Wait until the string "tomcat up" is in syslog, use regex character set to avoid self match
162
+ ansible.builtin.wait_for:
163
+ path: /var/log/syslog
164
+ search_regex: 'tomcat [u]p'
165
+
159
166
  - name: Wait until regex pattern matches in the file /tmp/foo and print the matched group
160
167
  ansible.builtin.wait_for:
161
168
  path: /tmp/foo
@@ -380,31 +387,29 @@ class LinuxTCPConnectionInfo(TCPConnectionInfo):
380
387
  if not os.path.isfile(self.source_file[family]):
381
388
  continue
382
389
  try:
383
- f = open(self.source_file[family])
384
- for tcp_connection in f.readlines():
385
- tcp_connection = tcp_connection.strip().split()
386
- if tcp_connection[self.local_address_field] == 'local_address':
387
- continue
388
- if (tcp_connection[self.connection_state_field] not in
389
- [get_connection_state_id(_connection_state) for _connection_state in self.module.params['active_connection_states']]):
390
- continue
391
- (local_ip, local_port) = tcp_connection[self.local_address_field].split(':')
392
- if self.port != local_port:
393
- continue
394
- (remote_ip, remote_port) = tcp_connection[self.remote_address_field].split(':')
395
- if (family, remote_ip) in self.exclude_ips:
396
- continue
397
- if any((
398
- (family, local_ip) in self.ips,
399
- (family, self.match_all_ips[family]) in self.ips,
400
- local_ip.startswith(self.ipv4_mapped_ipv6_address['prefix']) and
401
- (family, self.ipv4_mapped_ipv6_address['match_all']) in self.ips,
402
- )):
403
- active_connections += 1
404
- except IOError as e:
390
+ with open(self.source_file[family]) as f:
391
+ for tcp_connection in f.readlines():
392
+ tcp_connection = tcp_connection.strip().split()
393
+ if tcp_connection[self.local_address_field] == 'local_address':
394
+ continue
395
+ if (tcp_connection[self.connection_state_field] not in
396
+ [get_connection_state_id(_connection_state) for _connection_state in self.module.params['active_connection_states']]):
397
+ continue
398
+ (local_ip, local_port) = tcp_connection[self.local_address_field].split(':')
399
+ if self.port != local_port:
400
+ continue
401
+ (remote_ip, remote_port) = tcp_connection[self.remote_address_field].split(':')
402
+ if (family, remote_ip) in self.exclude_ips:
403
+ continue
404
+ if any((
405
+ (family, local_ip) in self.ips,
406
+ (family, self.match_all_ips[family]) in self.ips,
407
+ local_ip.startswith(self.ipv4_mapped_ipv6_address['prefix']) and
408
+ (family, self.ipv4_mapped_ipv6_address['match_all']) in self.ips,
409
+ )):
410
+ active_connections += 1
411
+ except OSError:
405
412
  pass
406
- finally:
407
- f.close()
408
413
 
409
414
  return active_connections
410
415
 
@@ -549,7 +554,7 @@ def main():
549
554
  try:
550
555
  if not os.access(b_path, os.F_OK):
551
556
  break
552
- except IOError:
557
+ except OSError:
553
558
  break
554
559
  elif port:
555
560
  try:
@@ -609,7 +614,7 @@ def main():
609
614
  break
610
615
  except Exception as e:
611
616
  module.warn('wait_for failed on "%s", unexpected exception(%s): %s.).' % (path, to_native(e.__class__), to_native(e)))
612
- except IOError:
617
+ except OSError:
613
618
  pass
614
619
  elif port:
615
620
  alt_connect_timeout = math.ceil(
@@ -648,8 +653,8 @@ def main():
648
653
  # Shutdown the client socket
649
654
  try:
650
655
  s.shutdown(socket.SHUT_RDWR)
651
- except socket.error as e:
652
- if e.errno != errno.ENOTCONN:
656
+ except OSError as ex:
657
+ if ex.errno != errno.ENOTCONN:
653
658
  raise
654
659
  # else, the server broke the connection on its end, assume it's not ready
655
660
  else:
@@ -661,8 +666,8 @@ def main():
661
666
  # Connection established, success!
662
667
  try:
663
668
  s.shutdown(socket.SHUT_RDWR)
664
- except socket.error as e:
665
- if e.errno != errno.ENOTCONN:
669
+ except OSError as ex:
670
+ if ex.errno != errno.ENOTCONN:
666
671
  raise
667
672
  # else, the server broke the connection on its end, assume it's not ready
668
673
  else:
@@ -502,10 +502,11 @@ class YumRepo:
502
502
  try:
503
503
  with open(self.dest, 'w') as fd:
504
504
  self.repofile.write(fd)
505
- except IOError as e:
505
+ except OSError as ex:
506
506
  self.module.fail_json(
507
- msg=f"Problems handling file {self.dest}.",
508
- details=to_native(e),
507
+ msg=f"Problems handling file {self.dest!r}.",
508
+ details=str(ex),
509
+ exception=ex,
509
510
  )
510
511
  else:
511
512
  try:
@@ -217,7 +217,7 @@ class DataLoader:
217
217
  except FileNotFoundError as ex:
218
218
  # DTFIX-FUTURE: why not just let the builtin one fly?
219
219
  raise AnsibleFileNotFound("Unable to retrieve file contents.", file_name=file_name) from ex
220
- except (IOError, OSError) as ex:
220
+ except OSError as ex:
221
221
  raise AnsibleParserError(f"An error occurred while trying to read the file {file_name!r}.") from ex
222
222
 
223
223
  data = Origin(path=file_name).tag(data)
@@ -448,7 +448,7 @@ class DataLoader:
448
448
 
449
449
  return real_path
450
450
 
451
- except (IOError, OSError) as ex:
451
+ except OSError as ex:
452
452
  raise AnsibleParserError(f"an error occurred while trying to read the file {to_text(real_path)!r}.") from ex
453
453
 
454
454
  def cleanup_tmp_file(self, file_path: str) -> None:
@@ -20,17 +20,17 @@ from __future__ import annotations
20
20
  import ansible.constants as C
21
21
  from ansible.errors import AnsibleParserError, AnsibleError, AnsibleAssertionError
22
22
  from ansible.module_utils._internal._datatag import AnsibleTagHelper
23
- from ansible.module_utils.six import string_types
24
23
  from ansible.module_utils.common.sentinel import Sentinel
25
24
  from ansible.module_utils.common.text.converters import to_text
26
25
  from ansible.parsing.splitter import parse_kv, split_args
27
26
  from ansible.parsing.vault import EncryptedString
28
27
  from ansible.plugins.loader import module_loader, action_loader
29
- from ansible._internal._templating._engine import TemplateEngine
28
+ from ansible._internal._templating import _jinja_bits
29
+ from ansible.utils.display import Display
30
30
  from ansible.utils.fqcn import add_internal_fqcns
31
31
 
32
32
 
33
- # modules formated for user msg
33
+ # modules formatted for user msg
34
34
  _BUILTIN_RAW_PARAM_MODULES_SIMPLE = set([
35
35
  'include_vars',
36
36
  'include_tasks',
@@ -152,38 +152,43 @@ class ModuleArgsParser:
152
152
  arguments can be fuzzy. Deal with all the forms.
153
153
  """
154
154
 
155
- additional_args = {} if additional_args is None else additional_args
156
-
157
155
  # final args are the ones we'll eventually return, so first update
158
156
  # them with any additional args specified, which have lower priority
159
157
  # than those which may be parsed/normalized next
160
158
  final_args = dict()
161
- if additional_args:
162
- if isinstance(additional_args, (str, EncryptedString)):
163
- # DTFIX5: should this be is_possibly_template?
164
- if TemplateEngine().is_template(additional_args):
165
- final_args['_variable_params'] = additional_args
166
- else:
167
- raise AnsibleParserError("Complex args containing variables cannot use bare variables (without Jinja2 delimiters), "
168
- "and must use the full variable style ('{{var_name}}')")
159
+
160
+ if additional_args is not Sentinel:
161
+ if isinstance(additional_args, str) and _jinja_bits.is_possibly_all_template(additional_args):
162
+ final_args['_variable_params'] = additional_args
169
163
  elif isinstance(additional_args, dict):
170
164
  final_args.update(additional_args)
165
+ elif additional_args is None:
166
+ Display().deprecated(
167
+ msg="Ignoring empty task `args` keyword.",
168
+ version="2.23",
169
+ help_text='A mapping or template which resolves to a mapping is required.',
170
+ obj=self._task_ds,
171
+ )
171
172
  else:
172
- raise AnsibleParserError('Complex args must be a dictionary or variable string ("{{var}}").')
173
+ raise AnsibleParserError(
174
+ message='The value of the task `args` keyword is invalid.',
175
+ help_text='A mapping or template which resolves to a mapping is required.',
176
+ obj=additional_args,
177
+ )
173
178
 
174
179
  # how we normalize depends if we figured out what the module name is
175
180
  # yet. If we have already figured it out, it's a 'new style' invocation.
176
181
  # otherwise, it's not
177
182
 
178
183
  if action is not None:
179
- args = self._normalize_new_style_args(thing, action)
184
+ args = self._normalize_new_style_args(thing, action, additional_args)
180
185
  else:
181
186
  (action, args) = self._normalize_old_style_args(thing)
182
187
 
183
188
  # this can occasionally happen, simplify
184
189
  if args and 'args' in args:
185
190
  tmp_args = args.pop('args')
186
- if isinstance(tmp_args, string_types):
191
+ if isinstance(tmp_args, str):
187
192
  tmp_args = parse_kv(tmp_args)
188
193
  args.update(tmp_args)
189
194
 
@@ -206,7 +211,7 @@ class ModuleArgsParser:
206
211
 
207
212
  return (action, final_args)
208
213
 
209
- def _normalize_new_style_args(self, thing, action):
214
+ def _normalize_new_style_args(self, thing, action, additional_args):
210
215
  """
211
216
  deals with fuzziness in new style module invocations
212
217
  accepting key=value pairs and dictionaries, and returns
@@ -222,11 +227,23 @@ class ModuleArgsParser:
222
227
  if isinstance(thing, dict):
223
228
  # form is like: { xyz: { x: 2, y: 3 } }
224
229
  args = thing
225
- elif isinstance(thing, string_types):
230
+ elif isinstance(thing, str):
226
231
  # form is like: copy: src=a dest=b
227
232
  check_raw = action in FREEFORM_ACTIONS
228
233
  args = parse_kv(thing, check_raw=check_raw)
234
+ args_keys = set(args) - {'_raw_params'}
235
+
236
+ if args_keys and additional_args is not Sentinel:
237
+ kv_args = ', '.join(repr(arg) for arg in sorted(args_keys))
238
+
239
+ Display().deprecated(
240
+ msg=f"Merging legacy k=v args ({kv_args}) into task args.",
241
+ help_text="Include all task args in the task `args` mapping.",
242
+ version="2.23",
243
+ obj=thing,
244
+ )
229
245
  elif isinstance(thing, EncryptedString):
246
+ # k=v parsing intentionally omitted
230
247
  args = dict(_raw_params=thing)
231
248
  elif thing is None:
232
249
  # this can happen with modules which take no params, like ping:
@@ -253,6 +270,7 @@ class ModuleArgsParser:
253
270
 
254
271
  if isinstance(thing, dict):
255
272
  # form is like: action: { module: 'copy', src: 'a', dest: 'b' }
273
+ Display().deprecated("Using a mapping for `action` is deprecated.", version='2.23', help_text='Use a string value for `action`.', obj=thing)
256
274
  thing = thing.copy()
257
275
  if 'module' in thing:
258
276
  action, module_args = self._split_module_string(thing['module'])
@@ -261,7 +279,7 @@ class ModuleArgsParser:
261
279
  args.update(parse_kv(module_args, check_raw=check_raw))
262
280
  del args['module']
263
281
 
264
- elif isinstance(thing, string_types):
282
+ elif isinstance(thing, str):
265
283
  # form is like: action: copy src=a dest=b
266
284
  (action, args) = self._split_module_string(thing)
267
285
  check_raw = action in FREEFORM_ACTIONS
@@ -287,7 +305,7 @@ class ModuleArgsParser:
287
305
  # This is the standard YAML form for command-type modules. We grab
288
306
  # the args and pass them in as additional arguments, which can/will
289
307
  # be overwritten via dict updates from the other arg sources below
290
- additional_args = self._task_ds.get('args', dict())
308
+ additional_args = self._task_ds.get('args', Sentinel)
291
309
 
292
310
  # We can have one of action, local_action, or module specified
293
311
  # action
@@ -17,7 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- import errno
21
20
  import fcntl
22
21
  import functools
23
22
  import os
@@ -414,8 +413,8 @@ class FileVaultSecret(VaultSecret):
414
413
  try:
415
414
  with open(filename, "rb") as f:
416
415
  vault_pass = f.read().strip()
417
- except (OSError, IOError) as e:
418
- raise AnsibleError("Could not read vault password file %s: %s" % (filename, e))
416
+ except OSError as ex:
417
+ raise AnsibleError(f"Could not read vault password file {filename!r}.") from ex
419
418
 
420
419
  b_vault_data, dummy = self.loader._decrypt_if_vault_data(vault_pass)
421
420
 
@@ -571,8 +570,8 @@ def match_encrypt_secret(secrets, encrypt_vault_id=None):
571
570
  return match_encrypt_vault_id_secret(secrets,
572
571
  encrypt_vault_id=encrypt_vault_id)
573
572
 
574
- # Find the best/first secret from secrets since we didnt specify otherwise
575
- # ie, consider all of the available secrets as matches
573
+ # Find the best/first secret from secrets since we didn't specify otherwise
574
+ # ie, consider all the available secrets as matches
576
575
  _vault_id_matchers = [_vault_id for _vault_id, dummy in secrets]
577
576
  best_secret = match_best_secret(secrets, _vault_id_matchers)
578
577
 
@@ -1071,13 +1070,10 @@ class VaultEditor:
1071
1070
  try:
1072
1071
  # create file with secure permissions
1073
1072
  fd = os.open(thefile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC, mode)
1074
- except OSError as ose:
1075
- # Want to catch FileExistsError, which doesn't exist in Python 2, so catch OSError
1076
- # and compare the error number to get equivalent behavior in Python 2/3
1077
- if ose.errno == errno.EEXIST:
1078
- raise AnsibleError('Vault file got recreated while we were operating on it: %s' % to_native(ose))
1079
-
1080
- raise AnsibleError('Problem creating temporary vault file: %s' % to_native(ose))
1073
+ except FileExistsError as ex:
1074
+ raise AnsibleError('Vault file got recreated while we were operating on it.') from ex
1075
+ except OSError as ex:
1076
+ raise AnsibleError('Problem creating temporary vault file.') from ex
1081
1077
 
1082
1078
  try:
1083
1079
  # now write to the file and ensure ours is only data in it
@@ -1417,7 +1413,7 @@ class EncryptedString(AnsibleTaggedObject):
1417
1413
  'ljust',
1418
1414
  'lower',
1419
1415
  'lstrip',
1420
- 'maketrans', # static, but implemented for simplicty/consistency
1416
+ 'maketrans', # static, but implemented for simplicity/consistency
1421
1417
  'partition',
1422
1418
  'removeprefix',
1423
1419
  'removesuffix',
ansible/playbook/base.py CHANGED
@@ -83,6 +83,11 @@ class _ClassProperty:
83
83
 
84
84
  class FieldAttributeBase:
85
85
 
86
+ _post_validate_object = False
87
+ """
88
+ `False` skips FieldAttribute post-validation on intermediate objects and mixins for attributes without `always_post_validate`.
89
+ Leaf objects (e.g., `Task`) should set this attribute `True` to opt-in to post-validation.
90
+ """
86
91
  fattributes = _ClassProperty()
87
92
 
88
93
  @classmethod
@@ -216,8 +221,6 @@ class FieldAttributeBase:
216
221
 
217
222
  def validate(self, all_vars=None):
218
223
  """ validation that is done at parse time, not load time """
219
- all_vars = {} if all_vars is None else all_vars
220
-
221
224
  if not self._validated:
222
225
  # walk all fields in the object
223
226
  for (name, attribute) in self.fattributes.items():
@@ -566,8 +569,8 @@ class FieldAttributeBase:
566
569
  # only import_role is checked here because import_tasks never reaches this point
567
570
  return Sentinel
568
571
 
569
- # FIXME: compare types, not strings
570
- if not attribute.always_post_validate and self.__class__.__name__ not in ('Task', 'Handler', 'PlayContext', 'IncludeRole', 'TaskInclude'):
572
+ # Skip post validation unless always_post_validate is True, or the object requires post validation.
573
+ if not attribute.always_post_validate and not self._post_validate_object:
571
574
  # Intermediate objects like Play() won't have their fields validated by
572
575
  # default, as their values are often inherited by other objects and validated
573
576
  # later, so we don't want them to fail out early
@@ -122,7 +122,7 @@ def load_list_of_tasks(ds, play, block=None, role=None, task_include=None, use_h
122
122
  except AnsibleParserError as ex:
123
123
  # if the raises exception was created with obj=ds args, then it includes the detail
124
124
  # so we dont need to add it so we can just re raise.
125
- if ex.obj:
125
+ if ex.obj is not None:
126
126
  raise
127
127
  # But if it wasn't, we can add the yaml object now to get more detail
128
128
  # DTFIX-FUTURE: this *should* be unnecessary- check code coverage.
@@ -144,7 +144,9 @@ class IncludedFile:
144
144
  parent_include_dir = parent_include._role_path
145
145
  else:
146
146
  try:
147
- parent_include_dir = os.path.dirname(parent_include.args.get('_raw_params'))
147
+ # FUTURE: Since the parent include path has already been resolved, it should be used here.
148
+ # Unfortunately it's not currently stored anywhere, so it must be calculated again.
149
+ parent_include_dir = os.path.dirname(templar.template(parent_include.args.get('_raw_params')))
148
150
  except AnsibleError as e:
149
151
  parent_include_dir = ''
150
152
  display.warning(
@@ -71,6 +71,8 @@ class PlayContext(Base):
71
71
  connection/authentication information.
72
72
  """
73
73
 
74
+ _post_validate_object = True
75
+
74
76
  # base
75
77
  module_compression = FieldAttribute(isa='string', default=C.DEFAULT_MODULE_COMPRESSION)
76
78
  shell = FieldAttribute(isa='string')