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.
- ansible/_internal/__init__.py +1 -4
- ansible/_internal/_ansiballz/_builder.py +1 -3
- ansible/_internal/_collection_proxy.py +7 -9
- ansible/_internal/_json/__init__.py +3 -4
- ansible/_internal/_templating/_engine.py +1 -1
- ansible/_internal/_templating/_jinja_plugins.py +1 -2
- ansible/_internal/_wrapt.py +105 -301
- ansible/cli/__init__.py +11 -10
- ansible/cli/adhoc.py +1 -2
- ansible/cli/arguments/option_helpers.py +1 -1
- ansible/cli/config.py +5 -6
- ansible/cli/doc.py +67 -67
- ansible/cli/galaxy.py +15 -24
- ansible/cli/inventory.py +0 -1
- ansible/cli/playbook.py +0 -1
- ansible/cli/pull.py +0 -1
- ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
- ansible/config/base.yml +1 -25
- ansible/config/manager.py +0 -2
- ansible/executor/play_iterator.py +42 -20
- ansible/executor/playbook_executor.py +0 -9
- ansible/executor/task_executor.py +26 -18
- ansible/executor/task_queue_manager.py +1 -3
- ansible/galaxy/api.py +33 -80
- ansible/galaxy/collection/__init__.py +11 -21
- ansible/galaxy/dependency_resolution/__init__.py +10 -9
- ansible/galaxy/dependency_resolution/dataclasses.py +86 -70
- ansible/galaxy/dependency_resolution/providers.py +54 -134
- ansible/galaxy/dependency_resolution/versioning.py +2 -4
- ansible/galaxy/role.py +1 -33
- ansible/inventory/manager.py +2 -3
- ansible/keyword_desc.yml +0 -3
- ansible/module_utils/_internal/_datatag/__init__.py +2 -10
- ansible/module_utils/_internal/_no_six.py +86 -0
- ansible/module_utils/_text.py +28 -8
- ansible/module_utils/ansible_release.py +2 -2
- ansible/module_utils/basic.py +26 -23
- ansible/module_utils/common/_collections_compat.py +11 -2
- ansible/module_utils/common/collections.py +8 -3
- ansible/module_utils/common/dict_transformations.py +1 -2
- ansible/module_utils/common/network.py +4 -2
- ansible/module_utils/common/parameters.py +32 -41
- ansible/module_utils/common/text/converters.py +109 -23
- ansible/module_utils/common/text/formatters.py +6 -2
- ansible/module_utils/common/validation.py +11 -9
- ansible/module_utils/connection.py +8 -3
- ansible/module_utils/facts/hardware/linux.py +23 -7
- ansible/module_utils/facts/hardware/netbsd.py +1 -1
- ansible/module_utils/facts/hardware/sunos.py +2 -1
- ansible/module_utils/facts/packages.py +6 -2
- ansible/module_utils/facts/system/distribution.py +2 -1
- ansible/module_utils/facts/system/env.py +6 -3
- ansible/module_utils/facts/system/local.py +3 -1
- ansible/module_utils/parsing/convert_bool.py +6 -2
- ansible/module_utils/service.py +2 -3
- ansible/module_utils/six/__init__.py +11 -6
- ansible/module_utils/yumdnf.py +0 -5
- ansible/modules/apt.py +18 -13
- ansible/modules/apt_repository.py +1 -1
- ansible/modules/assemble.py +5 -9
- ansible/modules/blockinfile.py +39 -23
- ansible/modules/cron.py +26 -35
- ansible/modules/deb822_repository.py +83 -12
- ansible/modules/dnf.py +3 -7
- ansible/modules/dnf5.py +4 -6
- ansible/modules/expect.py +0 -3
- ansible/modules/find.py +1 -2
- ansible/modules/get_url.py +1 -1
- ansible/modules/git.py +4 -5
- ansible/modules/include_vars.py +1 -1
- ansible/modules/known_hosts.py +7 -1
- ansible/modules/lineinfile.py +71 -63
- ansible/modules/package_facts.py +1 -1
- ansible/modules/pip.py +8 -2
- ansible/modules/replace.py +6 -6
- ansible/modules/service.py +3 -4
- ansible/modules/stat.py +20 -0
- ansible/modules/uri.py +9 -10
- ansible/modules/user.py +1 -2
- ansible/modules/wait_for.py +2 -2
- ansible/modules/wait_for_connection.py +2 -1
- ansible/modules/yum_repository.py +1 -16
- ansible/parsing/dataloader.py +24 -31
- ansible/parsing/vault/__init__.py +1 -2
- ansible/playbook/base.py +8 -56
- ansible/playbook/block.py +0 -60
- ansible/playbook/collectionsearch.py +1 -2
- ansible/playbook/handler.py +1 -7
- ansible/playbook/helpers.py +0 -7
- ansible/playbook/included_file.py +1 -1
- ansible/playbook/play.py +102 -36
- ansible/playbook/play_context.py +4 -0
- ansible/playbook/role/__init__.py +10 -65
- ansible/playbook/role/definition.py +3 -4
- ansible/playbook/role/include.py +2 -3
- ansible/playbook/role/metadata.py +1 -12
- ansible/playbook/role/requirement.py +1 -2
- ansible/playbook/role_include.py +1 -2
- ansible/playbook/taggable.py +16 -5
- ansible/playbook/task.py +11 -50
- ansible/plugins/action/__init__.py +20 -19
- ansible/plugins/action/add_host.py +1 -2
- ansible/plugins/action/fetch.py +3 -5
- ansible/plugins/action/group_by.py +1 -2
- ansible/plugins/action/include_vars.py +20 -22
- ansible/plugins/action/script.py +1 -3
- ansible/plugins/action/template.py +1 -2
- ansible/plugins/action/uri.py +4 -2
- ansible/plugins/cache/__init__.py +1 -0
- ansible/plugins/callback/__init__.py +13 -6
- ansible/plugins/connection/__init__.py +3 -7
- ansible/plugins/connection/local.py +2 -3
- ansible/plugins/connection/psrp.py +0 -2
- ansible/plugins/connection/ssh.py +2 -7
- ansible/plugins/connection/winrm.py +0 -2
- ansible/plugins/doc_fragments/result_format_callback.py +15 -0
- ansible/plugins/filter/core.py +4 -5
- ansible/plugins/filter/encryption.py +3 -27
- ansible/plugins/filter/mathstuff.py +1 -2
- ansible/plugins/filter/to_nice_yaml.yml +31 -3
- ansible/plugins/filter/to_yaml.yml +29 -12
- ansible/plugins/inventory/__init__.py +1 -2
- ansible/plugins/inventory/toml.py +3 -6
- ansible/plugins/inventory/yaml.py +1 -2
- ansible/plugins/loader.py +3 -4
- ansible/plugins/lookup/password.py +1 -2
- ansible/plugins/lookup/subelements.py +2 -3
- ansible/plugins/lookup/url.py +1 -1
- ansible/plugins/lookup/varnames.py +1 -2
- ansible/plugins/shell/__init__.py +9 -4
- ansible/plugins/shell/powershell.py +8 -24
- ansible/plugins/strategy/__init__.py +5 -2
- ansible/plugins/test/core.py +4 -1
- ansible/plugins/test/falsy.yml +1 -1
- ansible/plugins/test/regex.yml +18 -6
- ansible/plugins/test/truthy.yml +1 -1
- ansible/release.py +2 -2
- ansible/template/__init__.py +3 -7
- ansible/utils/collection_loader/_collection_config.py +5 -0
- ansible/utils/collection_loader/_collection_finder.py +11 -14
- ansible/utils/context_objects.py +7 -4
- ansible/utils/display.py +7 -6
- ansible/utils/encrypt.py +0 -5
- ansible/utils/helpers.py +6 -2
- ansible/utils/jsonrpc.py +7 -3
- ansible/utils/plugin_docs.py +49 -38
- ansible/utils/ssh_functions.py +0 -19
- ansible/utils/unsafe_proxy.py +7 -7
- ansible/vars/clean.py +2 -3
- ansible/vars/manager.py +28 -22
- ansible/vars/plugins.py +1 -31
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/METADATA +4 -4
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/RECORD +213 -214
- ansible_test/_data/completion/docker.txt +7 -7
- ansible_test/_data/completion/network.txt +0 -1
- ansible_test/_data/completion/remote.txt +4 -4
- ansible_test/_data/requirements/ansible-test.txt +1 -1
- ansible_test/_data/requirements/ansible.txt +1 -1
- ansible_test/_data/requirements/sanity.ansible-doc.txt +2 -2
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
- ansible_test/_data/requirements/sanity.import.txt +1 -1
- ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +6 -6
- ansible_test/_data/requirements/sanity.runtime-metadata.txt +1 -1
- ansible_test/_data/requirements/sanity.validate-modules.txt +2 -2
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/cache.py +2 -5
- ansible_test/_internal/cli/compat.py +1 -1
- ansible_test/_internal/commands/coverage/combine.py +1 -3
- ansible_test/_internal/commands/integration/__init__.py +3 -7
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/integration/coverage.py +1 -3
- ansible_test/_internal/commands/integration/filters.py +5 -10
- ansible_test/_internal/commands/sanity/pylint.py +11 -0
- ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
- ansible_test/_internal/commands/units/__init__.py +1 -13
- ansible_test/_internal/compat/packaging.py +2 -2
- ansible_test/_internal/compat/yaml.py +2 -2
- ansible_test/_internal/completion.py +2 -5
- ansible_test/_internal/config.py +2 -7
- ansible_test/_internal/coverage_util.py +1 -1
- ansible_test/_internal/delegation.py +2 -0
- ansible_test/_internal/docker_util.py +1 -1
- ansible_test/_internal/host_profiles.py +6 -11
- ansible_test/_internal/provider/__init__.py +2 -5
- ansible_test/_internal/provisioning.py +2 -5
- ansible_test/_internal/pypi_proxy.py +1 -1
- ansible_test/_internal/python_requirements.py +1 -1
- ansible_test/_internal/target.py +2 -6
- ansible_test/_internal/thread.py +1 -4
- ansible_test/_internal/util.py +9 -14
- ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +48 -45
- ansible_test/_util/controller/sanity/pylint/plugins/string_format.py +9 -7
- ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +51 -37
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
- ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
- ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
- ansible_test/_util/target/common/constants.py +2 -2
- ansible_test/_util/target/setup/bootstrap.sh +0 -6
- ansible/utils/py3compat.py +0 -27
- ansible_test/_data/pytest/config/legacy.ini +0 -4
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.4rc1.dist-info → ansible_core-2.20.0.dist-info}/top_level.txt +0 -0
ansible/parsing/dataloader.py
CHANGED
|
@@ -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(
|
|
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.
|
|
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 =
|
|
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(
|
|
239
|
+
if given.startswith(os.path.sep) or given.startswith('~'):
|
|
246
240
|
path = given
|
|
247
241
|
else:
|
|
248
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, (
|
|
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
|
-
|
|
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
|
-
|
|
483
|
+
b_full_path = b_path + to_bytes(ext)
|
|
493
484
|
elif ext:
|
|
494
|
-
|
|
485
|
+
b_full_path = b'.'.join([b_path, to_bytes(ext)])
|
|
495
486
|
else:
|
|
496
|
-
|
|
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(
|
|
494
|
+
found.extend(self._get_dir_vars_files(full_path, extensions))
|
|
502
495
|
else:
|
|
503
496
|
continue
|
|
504
497
|
else:
|
|
505
|
-
found.append(
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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'
|
|
664
|
-
attrs[name] = attr.
|
|
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.
|
|
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=
|
|
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
|
ansible/playbook/handler.py
CHANGED
|
@@ -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=
|
|
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
|
ansible/playbook/helpers.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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=
|
|
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=
|
|
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, (
|
|
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, (
|
|
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']
|
ansible/playbook/play_context.py
CHANGED
|
@@ -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)
|