ansible-core 2.19.0b6__py3-none-any.whl → 2.19.0rc1__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 (111) hide show
  1. ansible/_internal/_json/__init__.py +31 -20
  2. ansible/_internal/_json/_profiles/_legacy.py +1 -1
  3. ansible/_internal/_templating/_jinja_bits.py +46 -14
  4. ansible/_internal/_templating/_jinja_common.py +1 -1
  5. ansible/_internal/_templating/_jinja_plugins.py +5 -2
  6. ansible/_internal/_templating/_utils.py +2 -1
  7. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/dump_object.py +9 -0
  8. ansible/cli/__init__.py +2 -2
  9. ansible/cli/_ssh_askpass.py +37 -30
  10. ansible/cli/adhoc.py +6 -3
  11. ansible/cli/console.py +2 -2
  12. ansible/cli/doc.py +2 -2
  13. ansible/config/base.yml +9 -6
  14. ansible/executor/module_common.py +9 -6
  15. ansible/executor/powershell/psrp_put_file.ps1 +1 -1
  16. ansible/executor/task_executor.py +2 -2
  17. ansible/executor/task_queue_manager.py +34 -70
  18. ansible/executor/task_result.py +1 -1
  19. ansible/galaxy/api.py +2 -2
  20. ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
  21. ansible/galaxy/dependency_resolution/providers.py +3 -3
  22. ansible/inventory/group.py +6 -1
  23. ansible/inventory/host.py +6 -1
  24. ansible/module_utils/_internal/_datatag/__init__.py +6 -1
  25. ansible/module_utils/_internal/_deprecator.py +12 -1
  26. ansible/module_utils/ansible_release.py +1 -1
  27. ansible/module_utils/basic.py +14 -16
  28. ansible/module_utils/common/yaml.py +1 -1
  29. ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
  30. ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
  31. ansible/module_utils/facts/hardware/base.py +1 -1
  32. ansible/module_utils/facts/other/facter.py +1 -1
  33. ansible/module_utils/facts/system/distribution.py +2 -2
  34. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  35. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  36. ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
  37. ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
  38. ansible/module_utils/urls.py +1 -1
  39. ansible/modules/apt.py +9 -3
  40. ansible/modules/assemble.py +5 -3
  41. ansible/modules/expect.py +5 -5
  42. ansible/modules/hostname.py +2 -2
  43. ansible/modules/pip.py +9 -11
  44. ansible/modules/raw.py +2 -2
  45. ansible/modules/stat.py +1 -1
  46. ansible/modules/systemd.py +1 -1
  47. ansible/modules/systemd_service.py +1 -1
  48. ansible/modules/wait_for.py +10 -3
  49. ansible/parsing/mod_args.py +38 -20
  50. ansible/parsing/vault/__init__.py +3 -3
  51. ansible/playbook/base.py +0 -2
  52. ansible/playbook/helpers.py +1 -1
  53. ansible/playbook/playbook_include.py +23 -57
  54. ansible/playbook/role/__init__.py +40 -23
  55. ansible/plugins/action/__init__.py +2 -2
  56. ansible/plugins/action/assemble.py +2 -1
  57. ansible/plugins/action/assert.py +2 -2
  58. ansible/plugins/action/script.py +5 -4
  59. ansible/plugins/action/template.py +1 -1
  60. ansible/plugins/callback/__init__.py +77 -87
  61. ansible/plugins/callback/default.py +0 -3
  62. ansible/plugins/callback/junit.py +0 -6
  63. ansible/plugins/connection/ssh.py +13 -6
  64. ansible/plugins/filter/pow.yml +1 -1
  65. ansible/plugins/filter/root.yml +1 -1
  66. ansible/plugins/filter/strftime.yml +3 -3
  67. ansible/plugins/filter/to_uuid.yml +1 -1
  68. ansible/plugins/inventory/script.py +1 -1
  69. ansible/plugins/loader.py +5 -0
  70. ansible/plugins/lookup/password.py +4 -6
  71. ansible/release.py +1 -1
  72. ansible/utils/display.py +16 -26
  73. ansible/utils/path.py +1 -1
  74. ansible/utils/vars.py +6 -2
  75. ansible/vars/manager.py +6 -3
  76. ansible/vars/reserved.py +6 -4
  77. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/METADATA +1 -1
  78. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/RECORD +111 -109
  79. ansible_test/_internal/__init__.py +5 -0
  80. ansible_test/_internal/ansible_util.py +1 -1
  81. ansible_test/_internal/classification/python.py +6 -0
  82. ansible_test/_internal/cli/commands/__init__.py +0 -5
  83. ansible_test/_internal/cli/environments.py +51 -5
  84. ansible_test/_internal/commands/coverage/__init__.py +1 -1
  85. ansible_test/_internal/commands/integration/__init__.py +18 -5
  86. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  87. ansible_test/_internal/commands/sanity/__init__.py +3 -1
  88. ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
  89. ansible_test/_internal/commands/shell/__init__.py +43 -4
  90. ansible_test/_internal/commands/units/__init__.py +4 -1
  91. ansible_test/_internal/config.py +21 -13
  92. ansible_test/_internal/debugging.py +166 -0
  93. ansible_test/_internal/delegation.py +21 -13
  94. ansible_test/_internal/host_profiles.py +197 -6
  95. ansible_test/_internal/inventory.py +4 -0
  96. ansible_test/_internal/metadata.py +94 -4
  97. ansible_test/_internal/processes.py +80 -0
  98. ansible_test/_internal/python_requirements.py +27 -0
  99. ansible_test/_internal/target.py +8 -0
  100. ansible_test/_internal/util_common.py +13 -3
  101. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +2 -1
  102. ansible_test/_util/target/injector/python.py +8 -0
  103. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/WHEEL +0 -0
  104. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/entry_points.txt +0 -0
  105. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/COPYING +0 -0
  106. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  107. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  108. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  109. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  110. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  111. {ansible_core-2.19.0b6.dist-info → ansible_core-2.19.0rc1.dist-info}/top_level.txt +0 -0
@@ -80,7 +80,7 @@ attributes:
80
80
  bypass_host_loop:
81
81
  support: none
82
82
  check_mode:
83
- support: none
83
+ support: full
84
84
  diff_mode:
85
85
  support: full
86
86
  platform:
@@ -212,6 +212,7 @@ def main():
212
212
  decrypt=dict(type='bool', default=True),
213
213
  ),
214
214
  add_file_common_args=True,
215
+ supports_check_mode=True,
215
216
  )
216
217
 
217
218
  changed = False
@@ -266,12 +267,13 @@ def main():
266
267
  if backup and dest_hash is not None:
267
268
  result['backup_file'] = module.backup_local(dest)
268
269
 
269
- module.atomic_move(path, dest, unsafe_writes=module.params['unsafe_writes'])
270
+ if not module.check_mode:
271
+ module.atomic_move(path, dest, unsafe_writes=module.params['unsafe_writes'])
270
272
  changed = True
271
273
 
272
274
  cleanup(module, path, result)
273
275
 
274
- # handle file permissions
276
+ # handle file permissions (check mode aware)
275
277
  file_args = module.load_file_common_arguments(module.params)
276
278
  result['changed'] = module.set_fs_attributes_if_different(file_args, changed)
277
279
 
ansible/modules/expect.py CHANGED
@@ -218,7 +218,7 @@ def main():
218
218
  rc=0
219
219
  )
220
220
 
221
- startd = datetime.datetime.now()
221
+ start_date = datetime.datetime.now()
222
222
 
223
223
  try:
224
224
  try:
@@ -246,8 +246,8 @@ def main():
246
246
  except pexpect.ExceptionPexpect as e:
247
247
  module.fail_json(msg='%s' % to_native(e))
248
248
 
249
- endd = datetime.datetime.now()
250
- delta = endd - startd
249
+ end_date = datetime.datetime.now()
250
+ delta = end_date - start_date
251
251
 
252
252
  if b_out is None:
253
253
  b_out = b''
@@ -256,8 +256,8 @@ def main():
256
256
  cmd=args,
257
257
  stdout=to_native(b_out).rstrip('\r\n'),
258
258
  rc=rc,
259
- start=str(startd),
260
- end=str(endd),
259
+ start=str(start_date),
260
+ end=str(end_date),
261
261
  delta=str(delta),
262
262
  changed=True,
263
263
  )
@@ -608,8 +608,8 @@ class Hostname(object):
608
608
  self.use = module.params['use']
609
609
 
610
610
  if self.use is not None:
611
- strat = globals()['%sStrategy' % STRATS[self.use]]
612
- self.strategy = strat(module)
611
+ strategy = globals()['%sStrategy' % STRATS[self.use]]
612
+ self.strategy = strategy(module)
613
613
  elif platform.system() == 'Linux' and ServiceMgrFactCollector.is_systemd_managed(module):
614
614
  # This is Linux and systemd is active
615
615
  self.strategy = SystemdStrategy(module)
ansible/modules/pip.py CHANGED
@@ -60,7 +60,7 @@ options:
60
60
  virtualenv_python:
61
61
  description:
62
62
  - The Python executable used for creating the virtual environment.
63
- For example V(python3.12), V(python2.7). When not specified, the
63
+ For example V(python3.13). When not specified, the
64
64
  Python version used to run the ansible module is used. This parameter
65
65
  should not be used when O(virtualenv_command) is using V(pyvenv) or
66
66
  the C(-m venv) module.
@@ -93,8 +93,8 @@ options:
93
93
  description:
94
94
  - The explicit executable or pathname for the C(pip) executable,
95
95
  if different from the Ansible Python interpreter. For
96
- example V(pip3.3), if there are both Python 2.7 and 3.3 installations
97
- in the system and you want to run pip for the Python 3.3 installation.
96
+ example V(pip3.13), if there are multiple Python installations
97
+ in the system and you want to run pip for the Python 3.13 installation.
98
98
  - Mutually exclusive with O(virtualenv) (added in 2.1).
99
99
  - Does not affect the Ansible Python interpreter.
100
100
  - The C(setuptools) package must be installed for both the Ansible Python interpreter
@@ -134,7 +134,7 @@ notes:
134
134
  the virtualenv needs to be created.
135
135
  - Although it executes using the Ansible Python interpreter, the pip module shells out to
136
136
  run the actual pip command, so it can use any pip version you specify with O(executable).
137
- By default, it uses the pip version for the Ansible Python interpreter. For example, pip3 on python 3, and pip2 or pip on python 2.
137
+ By default, it uses the pip version for the Ansible Python interpreter.
138
138
  - The interpreter used by Ansible
139
139
  (see R(ansible_python_interpreter, ansible_python_interpreter))
140
140
  requires the setuptools package, regardless of the version of pip set with
@@ -197,11 +197,11 @@ EXAMPLES = """
197
197
  virtualenv: /my_app/venv
198
198
  virtualenv_site_packages: yes
199
199
 
200
- - name: Install bottle into the specified (virtualenv), using Python 2.7
200
+ - name: Install bottle into the specified (virtualenv), using Python 3.13
201
201
  ansible.builtin.pip:
202
202
  name: bottle
203
203
  virtualenv: /my_app/venv
204
- virtualenv_command: virtualenv-2.7
204
+ virtualenv_command: virtualenv-3.13
205
205
 
206
206
  - name: Install bottle within a user home directory
207
207
  ansible.builtin.pip:
@@ -227,10 +227,10 @@ EXAMPLES = """
227
227
  requirements: /my_app/requirements.txt
228
228
  extra_args: "--no-index --find-links=file:///my_downloaded_packages_dir"
229
229
 
230
- - name: Install bottle for Python 3.3 specifically, using the 'pip3.3' executable
230
+ - name: Install bottle for Python 3.13 specifically, using the 'pip3.13' executable
231
231
  ansible.builtin.pip:
232
232
  name: bottle
233
- executable: pip3.3
233
+ executable: pip3.13
234
234
 
235
235
  - name: Install bottle, forcing reinstallation if it's already installed
236
236
  ansible.builtin.pip:
@@ -460,9 +460,7 @@ def _get_pip(module, env=None, executable=None):
460
460
  candidate_pip_basenames = (executable,)
461
461
  elif executable is None and env is None and _have_pip_module():
462
462
  # If no executable or virtualenv were specified, use the pip module for the current Python interpreter if available.
463
- # Use of `__main__` is required to support Python 2.6 since support for executing packages with `runpy` was added in Python 2.7.
464
- # Without it Python 2.6 gives the following error: pip is a package and cannot be directly executed
465
- pip = [sys.executable, '-m', 'pip.__main__']
463
+ pip = [sys.executable, '-m', 'pip']
466
464
 
467
465
  if pip is None:
468
466
  if env is None:
ansible/modules/raw.py CHANGED
@@ -73,8 +73,8 @@ author:
73
73
  """
74
74
 
75
75
  EXAMPLES = r"""
76
- - name: Bootstrap a host without python2 installed
77
- ansible.builtin.raw: dnf install -y python2 python2-dnf libselinux-python
76
+ - name: Bootstrap a host without Python installed
77
+ ansible.builtin.raw: dnf install -y python3 python3-libdnf
78
78
 
79
79
  - name: Run a command that uses non-posix shell-isms (in this example /bin/sh doesn't handle redirection and wildcards together but bash does)
80
80
  ansible.builtin.raw: cat < /tmp/*txt
ansible/modules/stat.py CHANGED
@@ -408,7 +408,7 @@ def format_output(module, path, st):
408
408
  ('st_blksize', 'block_size'),
409
409
  ('st_rdev', 'device_type'),
410
410
  ('st_flags', 'flags'),
411
- # Some Berkley based
411
+ # Some Berkeley based
412
412
  ('st_gen', 'generation'),
413
413
  ('st_birthtime', 'birthtime'),
414
414
  # RISCOS
@@ -34,7 +34,7 @@ options:
34
34
  choices: [ reloaded, restarted, started, stopped ]
35
35
  enabled:
36
36
  description:
37
- - Whether the unit should start on boot. At least one of O(state) and O(enabled) are required.
37
+ - Whether the unit should start on boot. At least one of O(state) or O(enabled) are required.
38
38
  - If set, requires O(name).
39
39
  type: bool
40
40
  force:
@@ -34,7 +34,7 @@ options:
34
34
  choices: [ reloaded, restarted, started, stopped ]
35
35
  enabled:
36
36
  description:
37
- - Whether the unit should start on boot. At least one of O(state) and O(enabled) are required.
37
+ - Whether the unit should start on boot. At least one of O(state) or O(enabled) are required.
38
38
  - If set, requires O(name).
39
39
  type: bool
40
40
  force:
@@ -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
@@ -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
@@ -570,8 +570,8 @@ def match_encrypt_secret(secrets, encrypt_vault_id=None):
570
570
  return match_encrypt_vault_id_secret(secrets,
571
571
  encrypt_vault_id=encrypt_vault_id)
572
572
 
573
- # Find the best/first secret from secrets since we didnt specify otherwise
574
- # 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
575
575
  _vault_id_matchers = [_vault_id for _vault_id, dummy in secrets]
576
576
  best_secret = match_best_secret(secrets, _vault_id_matchers)
577
577
 
@@ -1413,7 +1413,7 @@ class EncryptedString(AnsibleTaggedObject):
1413
1413
  'ljust',
1414
1414
  'lower',
1415
1415
  'lstrip',
1416
- 'maketrans', # static, but implemented for simplicty/consistency
1416
+ 'maketrans', # static, but implemented for simplicity/consistency
1417
1417
  'partition',
1418
1418
  'removeprefix',
1419
1419
  'removesuffix',
ansible/playbook/base.py CHANGED
@@ -221,8 +221,6 @@ class FieldAttributeBase:
221
221
 
222
222
  def validate(self, all_vars=None):
223
223
  """ validation that is done at parse time, not load time """
224
- all_vars = {} if all_vars is None else all_vars
225
-
226
224
  if not self._validated:
227
225
  # walk all fields in the object
228
226
  for (name, attribute) in self.fattributes.items():
@@ -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.
@@ -19,12 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import os
21
21
 
22
- import ansible.constants as C
23
- from ansible.errors import AnsibleParserError, AnsibleAssertionError
24
22
  from ansible.module_utils.common.text.converters import to_bytes
25
- from ansible.module_utils._internal._datatag import AnsibleTagHelper
26
- from ansible.module_utils.six import string_types
27
- from ansible.parsing.splitter import split_args
28
23
  from ansible.playbook.attribute import NonInheritableFieldAttribute
29
24
  from ansible.playbook.base import Base
30
25
  from ansible.playbook.conditional import Conditional
@@ -32,15 +27,27 @@ from ansible.playbook.taggable import Taggable
32
27
  from ansible.utils.collection_loader import AnsibleCollectionConfig
33
28
  from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path, _get_collection_playbook_path
34
29
  from ansible._internal._templating._engine import TemplateEngine
35
- from ansible.utils.display import Display
36
-
37
- display = Display()
30
+ from ansible.errors import AnsibleError
31
+ from ansible import constants as C
38
32
 
39
33
 
40
34
  class PlaybookInclude(Base, Conditional, Taggable):
41
35
 
42
- import_playbook = NonInheritableFieldAttribute(isa='string')
43
- vars_val = NonInheritableFieldAttribute(isa='dict', default=dict, alias='vars')
36
+ import_playbook = NonInheritableFieldAttribute(isa='string', required=True)
37
+
38
+ _post_validate_object = True # manually post_validate to get free arg validation/coercion
39
+
40
+ def preprocess_data(self, ds):
41
+ keys = {action for action in C._ACTION_IMPORT_PLAYBOOK if action in ds}
42
+
43
+ if len(keys) != 1:
44
+ raise AnsibleError(f'Found conflicting import_playbook actions: {", ".join(sorted(keys))}')
45
+
46
+ key = next(iter(keys))
47
+
48
+ ds['import_playbook'] = ds.pop(key)
49
+
50
+ return ds
44
51
 
45
52
  @staticmethod
46
53
  def load(data, basedir, variable_manager=None, loader=None):
@@ -62,18 +69,22 @@ class PlaybookInclude(Base, Conditional, Taggable):
62
69
  new_obj = super(PlaybookInclude, self).load_data(ds, variable_manager, loader)
63
70
 
64
71
  all_vars = self.vars.copy()
72
+
65
73
  if variable_manager:
66
74
  all_vars |= variable_manager.get_vars()
67
75
 
68
76
  templar = TemplateEngine(loader=loader, variables=all_vars)
69
77
 
78
+ new_obj.post_validate(templar)
79
+
70
80
  # then we use the object to load a Playbook
71
81
  pb = Playbook(loader=loader)
72
82
 
73
- file_name = templar.template(new_obj.import_playbook)
83
+ file_name = new_obj.import_playbook
74
84
 
75
85
  # check for FQCN
76
86
  resource = _get_collection_playbook_path(file_name)
87
+
77
88
  if resource is not None:
78
89
  playbook = resource[1]
79
90
  playbook_collection = resource[2]
@@ -92,6 +103,7 @@ class PlaybookInclude(Base, Conditional, Taggable):
92
103
  else:
93
104
  # it is NOT a collection playbook, setup adjacent paths
94
105
  AnsibleCollectionConfig.playbook_paths.append(os.path.dirname(os.path.abspath(to_bytes(playbook, errors='surrogate_or_strict'))))
106
+ # broken, see: https://github.com/ansible/ansible/issues/85357
95
107
 
96
108
  pb._load_playbook_data(file_name=playbook, variable_manager=variable_manager, vars=self.vars.copy())
97
109
 
@@ -120,49 +132,3 @@ class PlaybookInclude(Base, Conditional, Taggable):
120
132
  task_block._when = new_obj.when[:] + task_block.when[:]
121
133
 
122
134
  return pb
123
-
124
- def preprocess_data(self, ds):
125
- """
126
- Reorganizes the data for a PlaybookInclude datastructure to line
127
- up with what we expect the proper attributes to be
128
- """
129
-
130
- if not isinstance(ds, dict):
131
- raise AnsibleAssertionError('ds (%s) should be a dict but was a %s' % (ds, type(ds)))
132
-
133
- # the new, cleaned datastructure, which will have legacy items reduced to a standard structure suitable for the
134
- # attributes of the task class; copy any tagged data to preserve things like origin
135
- new_ds = AnsibleTagHelper.tag_copy(ds, {})
136
-
137
- for (k, v) in ds.items():
138
- if k in C._ACTION_IMPORT_PLAYBOOK:
139
- self._preprocess_import(ds, new_ds, k, v)
140
- else:
141
- # some basic error checking, to make sure vars are properly
142
- # formatted and do not conflict with k=v parameters
143
- if k == 'vars':
144
- if 'vars' in new_ds:
145
- raise AnsibleParserError("import_playbook parameters cannot be mixed with 'vars' entries for import statements", obj=ds)
146
- elif not isinstance(v, dict):
147
- raise AnsibleParserError("vars for import_playbook statements must be specified as a dictionary", obj=ds)
148
- new_ds[k] = v
149
-
150
- return super(PlaybookInclude, self).preprocess_data(new_ds)
151
-
152
- def _preprocess_import(self, ds, new_ds, k, v):
153
- """
154
- Splits the playbook import line up into filename and parameters
155
- """
156
- if v is None:
157
- raise AnsibleParserError("playbook import parameter is missing", obj=ds)
158
- elif not isinstance(v, string_types):
159
- raise AnsibleParserError("playbook import parameter must be a string indicating a file path, got %s instead" % type(v), obj=ds)
160
-
161
- # The import_playbook line must include at least one item, which is the filename
162
- # to import. Anything after that should be regarded as a parameter to the import
163
- items = split_args(v)
164
- if len(items) == 0:
165
- raise AnsibleParserError("import_playbook statements must specify the file name to import", obj=ds)
166
-
167
- # DTFIX3: investigate this as a possible "problematic strip"
168
- new_ds['import_playbook'] = AnsibleTagHelper.tag_copy(v, items[0].strip())
@@ -18,6 +18,7 @@
18
18
  from __future__ import annotations
19
19
 
20
20
  import os
21
+ import typing as _t
21
22
 
22
23
  from collections.abc import Container, Mapping, Set, Sequence
23
24
  from types import MappingProxyType
@@ -39,6 +40,16 @@ from ansible.utils.collection_loader import AnsibleCollectionConfig
39
40
  from ansible.utils.path import is_subpath
40
41
  from ansible.utils.vars import combine_vars
41
42
 
43
+ # NOTE: This import is only needed for the type-checking in __init__. While there's an alternative
44
+ # available by using forward references this seems not to work well with commonly used IDEs.
45
+ # Therefore the TYPE_CHECKING hack seems to be a more universal approach, even if not being very elegant.
46
+ # References:
47
+ # * https://stackoverflow.com/q/39740632/199513
48
+ # * https://peps.python.org/pep-0484/#forward-references
49
+ if _t.TYPE_CHECKING:
50
+ from ansible.playbook.block import Block
51
+ from ansible.playbook.play import Play
52
+
42
53
  __all__ = ['Role', 'hash_params']
43
54
 
44
55
  # TODO: this should be a utility function, but can't be a member of
@@ -97,13 +108,19 @@ def hash_params(params):
97
108
 
98
109
  class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
99
110
 
100
- def __init__(self, play=None, from_files=None, from_include=False, validate=True, public=None, static=True):
101
- self._role_name = None
102
- self._role_path = None
103
- self._role_collection = None
104
- self._role_params = dict()
111
+ def __init__(self,
112
+ play: Play = None,
113
+ from_files: dict[str, list[str]] = None,
114
+ from_include: bool = False,
115
+ validate: bool = True,
116
+ public: bool = None,
117
+ static: bool = True) -> None:
118
+ self._role_name: str = None
119
+ self._role_path: str = None
120
+ self._role_collection: str = None
121
+ self._role_params: dict[str, dict[str, str]] = dict()
105
122
  self._loader = None
106
- self.static = static
123
+ self.static: bool = static
107
124
 
108
125
  # includes (static=false) default to private, while imports (static=true) default to public
109
126
  # but both can be overridden by global config if set
@@ -116,26 +133,26 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
116
133
  else:
117
134
  self.public = public
118
135
 
119
- self._metadata = RoleMetadata()
120
- self._play = play
121
- self._parents = []
122
- self._dependencies = []
123
- self._all_dependencies = None
124
- self._task_blocks = []
125
- self._handler_blocks = []
126
- self._compiled_handler_blocks = None
127
- self._default_vars = dict()
128
- self._role_vars = dict()
129
- self._had_task_run = dict()
130
- self._completed = dict()
131
- self._should_validate = validate
136
+ self._metadata: RoleMetadata = RoleMetadata()
137
+ self._play: Play = play
138
+ self._parents: list[Role] = []
139
+ self._dependencies: list[Role] = []
140
+ self._all_dependencies: list[Role] | None = None
141
+ self._task_blocks: list[Block] = []
142
+ self._handler_blocks: list[Block] = []
143
+ self._compiled_handler_blocks: list[Block] | None = None
144
+ self._default_vars: dict[str, str] | None = dict()
145
+ self._role_vars: dict[str, str] | None = dict()
146
+ self._had_task_run: dict[str, bool] = dict()
147
+ self._completed: dict[str, bool] = dict()
148
+ self._should_validate: bool = validate
132
149
 
133
150
  if from_files is None:
134
151
  from_files = {}
135
- self._from_files = from_files
152
+ self._from_files: dict[str, list[str]] = from_files
136
153
 
137
154
  # Indicates whether this role was included via include/import_role
138
- self.from_include = from_include
155
+ self.from_include: bool = from_include
139
156
 
140
157
  self._hash = None
141
158
 
@@ -357,8 +374,8 @@ class Role(Base, Conditional, Taggable, CollectionSearch, Delegatable):
357
374
  task_name = task_name + ' - ' + argument_spec['short_description']
358
375
 
359
376
  return {
360
- 'action': {
361
- 'module': 'ansible.builtin.validate_argument_spec',
377
+ 'action': 'ansible.builtin.validate_argument_spec',
378
+ 'args': {
362
379
  # Pass only the 'options' portion of the arg spec to the module.
363
380
  'argument_spec': argument_spec.get('options', {}),
364
381
  'provided_arguments': self._role_params,
@@ -103,7 +103,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
103
103
  self._display = display
104
104
 
105
105
  @abstractmethod
106
- def run(self, tmp=None, task_vars=None):
106
+ def run(self, tmp: str | None = None, task_vars: dict[str, t.Any] | None = None) -> dict[str, t.Any]:
107
107
  """ Action Plugins should implement this method to perform their
108
108
  tasks. Everything else in this base class is a helper method for the
109
109
  action plugin to do that.
@@ -120,7 +120,7 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
120
120
  * Module parameters. These are stored in self._task.args
121
121
  """
122
122
  # does not default to {'changed': False, 'failed': False}, as it used to break async
123
- result = {}
123
+ result: dict[str, t.Any] = {}
124
124
 
125
125
  if tmp is not None:
126
126
  display.warning('ActionModule.run() no longer honors the tmp parameter. Action'