ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b5__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 (163) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_collection_proxy.py +1 -1
  3. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  4. ansible/_internal/_errors/_captured.py +25 -30
  5. ansible/_internal/_errors/_error_factory.py +89 -0
  6. ansible/_internal/_errors/_error_utils.py +240 -0
  7. ansible/_internal/_errors/_task_timeout.py +28 -0
  8. ansible/_internal/_event_formatting.py +127 -0
  9. ansible/_internal/_json/__init__.py +5 -5
  10. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  11. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  12. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  13. ansible/_internal/_ssh/__init__.py +0 -0
  14. ansible/_internal/_ssh/_agent_launch.py +91 -0
  15. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  16. ansible/_internal/_templating/__init__.py +5 -3
  17. ansible/_internal/_templating/_datatag.py +2 -1
  18. ansible/_internal/_templating/_engine.py +3 -4
  19. ansible/_internal/_templating/_jinja_bits.py +21 -16
  20. ansible/_internal/_templating/_jinja_common.py +18 -27
  21. ansible/_internal/_templating/_jinja_plugins.py +31 -3
  22. ansible/_internal/_templating/_lazy_containers.py +5 -5
  23. ansible/_internal/_templating/_transform.py +20 -19
  24. ansible/_internal/_templating/_utils.py +1 -1
  25. ansible/_internal/_yaml/_dumper.py +1 -1
  26. ansible/_internal/_yaml/_errors.py +7 -7
  27. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  29. ansible/cli/__init__.py +5 -82
  30. ansible/cli/arguments/option_helpers.py +2 -3
  31. ansible/cli/doc.py +84 -28
  32. ansible/cli/inventory.py +1 -1
  33. ansible/compat/importlib_resources.py +9 -12
  34. ansible/config/base.yml +22 -0
  35. ansible/errors/__init__.py +96 -49
  36. ansible/executor/module_common.py +8 -10
  37. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  38. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  39. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  40. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  41. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  42. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  43. ansible/executor/powershell/module_manifest.py +52 -0
  44. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  45. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  46. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  47. ansible/executor/process/worker.py +38 -113
  48. ansible/executor/task_executor.py +26 -61
  49. ansible/executor/task_result.py +2 -4
  50. ansible/galaxy/collection/__init__.py +1 -4
  51. ansible/inventory/manager.py +1 -1
  52. ansible/module_utils/_internal/__init__.py +0 -3
  53. ansible/module_utils/_internal/_ambient_context.py +3 -3
  54. ansible/module_utils/_internal/_ansiballz.py +4 -2
  55. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  56. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  57. ansible/module_utils/_internal/_deprecator.py +66 -48
  58. ansible/module_utils/_internal/_errors.py +88 -17
  59. ansible/module_utils/_internal/_event_utils.py +61 -0
  60. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  61. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  62. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  63. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  64. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  65. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  66. ansible/module_utils/_internal/_plugin_info.py +1 -1
  67. ansible/module_utils/_internal/_stack.py +22 -0
  68. ansible/module_utils/_internal/_text_utils.py +6 -0
  69. ansible/module_utils/_internal/_traceback.py +11 -8
  70. ansible/module_utils/ansible_release.py +1 -1
  71. ansible/module_utils/basic.py +49 -15
  72. ansible/module_utils/common/arg_spec.py +2 -2
  73. ansible/module_utils/common/collections.py +6 -0
  74. ansible/module_utils/common/json.py +2 -2
  75. ansible/module_utils/common/text/converters.py +3 -3
  76. ansible/module_utils/common/validation.py +1 -1
  77. ansible/module_utils/common/warnings.py +80 -23
  78. ansible/module_utils/common/yaml.py +1 -1
  79. ansible/module_utils/datatag.py +5 -2
  80. ansible/module_utils/facts/system/distribution.py +16 -3
  81. ansible/module_utils/facts/virtual/linux.py +1 -1
  82. ansible/module_utils/service.py +2 -9
  83. ansible/modules/apt_repository.py +7 -29
  84. ansible/modules/async_status.py +13 -11
  85. ansible/modules/async_wrapper.py +5 -5
  86. ansible/modules/dnf5.py +14 -22
  87. ansible/modules/hostname.py +0 -1
  88. ansible/modules/service.py +3 -9
  89. ansible/parsing/ajson.py +3 -5
  90. ansible/parsing/dataloader.py +4 -4
  91. ansible/parsing/mod_args.py +1 -1
  92. ansible/parsing/plugin_docs.py +2 -2
  93. ansible/parsing/utils/yaml.py +3 -3
  94. ansible/parsing/vault/__init__.py +4 -4
  95. ansible/playbook/playbook_include.py +1 -1
  96. ansible/playbook/taggable.py +0 -3
  97. ansible/plugins/__init__.py +0 -25
  98. ansible/plugins/action/__init__.py +8 -31
  99. ansible/plugins/action/add_host.py +1 -1
  100. ansible/plugins/action/assemble.py +8 -16
  101. ansible/plugins/action/async_status.py +7 -2
  102. ansible/plugins/action/copy.py +8 -7
  103. ansible/plugins/action/gather_facts.py +8 -8
  104. ansible/plugins/action/package.py +5 -8
  105. ansible/plugins/action/script.py +8 -15
  106. ansible/plugins/action/service.py +3 -7
  107. ansible/plugins/action/template.py +3 -8
  108. ansible/plugins/action/unarchive.py +5 -15
  109. ansible/plugins/action/uri.py +9 -20
  110. ansible/plugins/callback/__init__.py +4 -6
  111. ansible/plugins/callback/junit.py +4 -2
  112. ansible/plugins/connection/local.py +2 -2
  113. ansible/plugins/connection/ssh.py +17 -9
  114. ansible/plugins/connection/winrm.py +5 -2
  115. ansible/plugins/doc_fragments/constructed.py +2 -2
  116. ansible/plugins/filter/core.py +13 -6
  117. ansible/plugins/filter/encryption.py +4 -4
  118. ansible/plugins/inventory/__init__.py +11 -10
  119. ansible/plugins/inventory/script.py +1 -1
  120. ansible/plugins/list.py +69 -16
  121. ansible/plugins/loader.py +7 -7
  122. ansible/plugins/lookup/csvfile.py +16 -71
  123. ansible/plugins/lookup/first_found.py +2 -1
  124. ansible/plugins/shell/__init__.py +56 -2
  125. ansible/plugins/shell/powershell.py +66 -9
  126. ansible/plugins/shell/sh.py +9 -5
  127. ansible/plugins/test/core.py +21 -15
  128. ansible/plugins/test/finished.yml +1 -1
  129. ansible/plugins/test/uri.py +2 -5
  130. ansible/release.py +1 -1
  131. ansible/template/__init__.py +30 -2
  132. ansible/utils/display.py +103 -128
  133. ansible/utils/hashing.py +0 -1
  134. ansible/utils/listify.py +6 -4
  135. ansible/utils/unsafe_proxy.py +1 -1
  136. ansible/vars/hostvars.py +1 -1
  137. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +1 -1
  138. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +162 -151
  139. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  140. ansible_test/_data/completion/docker.txt +3 -3
  141. ansible_test/_data/completion/remote.txt +1 -0
  142. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  143. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  144. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  145. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  146. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  147. ansible_test/_internal/util.py +20 -0
  148. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  149. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  150. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  151. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  152. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  153. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  154. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  155. ansible/_internal/_errors/_utils.py +0 -310
  156. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  157. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/COPYING +0 -0
  158. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  159. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  160. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  161. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  162. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  163. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
ansible/cli/doc.py CHANGED
@@ -9,12 +9,14 @@ from __future__ import annotations
9
9
  # ansible.cli needs to be imported first, to ensure the source bin/* scripts run that code first
10
10
  from ansible.cli import CLI
11
11
 
12
+ import collections.abc
12
13
  import importlib
13
14
  import pkgutil
14
15
  import os
15
16
  import os.path
16
17
  import re
17
18
  import textwrap
19
+ import typing as t
18
20
 
19
21
  import yaml
20
22
 
@@ -35,7 +37,7 @@ from ansible.parsing.plugin_docs import read_docstub
35
37
  from ansible.parsing.yaml.dumper import AnsibleDumper
36
38
  from ansible.parsing.yaml.loader import AnsibleLoader
37
39
  from ansible._internal._yaml._loader import AnsibleInstrumentedLoader
38
- from ansible.plugins.list import list_plugins
40
+ from ansible.plugins.list import _list_plugins_with_info, _PluginDocMetadata
39
41
  from ansible.plugins.loader import action_loader, fragment_loader
40
42
  from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
41
43
  from ansible.utils.collection_loader._collection_finder import _get_collection_name_from_path
@@ -44,6 +46,7 @@ from ansible.utils.display import Display
44
46
  from ansible.utils.plugin_docs import get_plugin_docs, get_docstring, get_versioned_doclink
45
47
  from ansible.template import trust_as_template
46
48
  from ansible._internal import _json
49
+ from ansible._internal._templating import _jinja_plugins
47
50
 
48
51
  display = Display()
49
52
 
@@ -788,35 +791,47 @@ class DocCLI(CLI, RoleMixin):
788
791
  return coll_filter
789
792
 
790
793
  def _list_plugins(self, plugin_type, content):
791
-
792
- results = {}
793
- self.plugins = {}
794
- loader = DocCLI._prep_loader(plugin_type)
794
+ DocCLI._prep_loader(plugin_type)
795
795
 
796
796
  coll_filter = self._get_collection_filter()
797
- self.plugins.update(list_plugins(plugin_type, coll_filter))
797
+ plugins = _list_plugins_with_info(plugin_type, coll_filter)
798
+
799
+ # Remove the internal ansible._protomatter plugins if getting all plugins
800
+ if not coll_filter:
801
+ plugins = {k: v for k, v in plugins.items() if not k.startswith('ansible._protomatter.')}
798
802
 
799
803
  # get appropriate content depending on option
800
804
  if content == 'dir':
801
- results = self._get_plugin_list_descriptions(loader)
805
+ results = self._get_plugin_list_descriptions(plugins)
802
806
  elif content == 'files':
803
- results = {k: self.plugins[k][0] for k in self.plugins.keys()}
807
+ results = {k: v.path for k, v in plugins.items()}
804
808
  else:
805
- results = {k: {} for k in self.plugins.keys()}
809
+ results = {k: {} for k in plugins.keys()}
806
810
  self.plugin_list = set() # reset for next iteration
807
811
 
808
812
  return results
809
813
 
810
- def _get_plugins_docs(self, plugin_type, names, fail_ok=False, fail_on_errors=True):
811
-
814
+ def _get_plugins_docs(self, plugin_type: str, names: collections.abc.Iterable[str], fail_ok: bool = False, fail_on_errors: bool = True) -> dict[str, dict]:
812
815
  loader = DocCLI._prep_loader(plugin_type)
813
816
 
817
+ if plugin_type in ('filter', 'test'):
818
+ jinja2_builtins = _jinja_plugins.get_jinja_builtin_plugin_descriptions(plugin_type)
819
+ jinja2_builtins.update({name.split('.')[-1]: value for name, value in jinja2_builtins.items()}) # add short-named versions for lookup
820
+ else:
821
+ jinja2_builtins = {}
822
+
814
823
  # get the docs for plugins in the command line list
815
824
  plugin_docs = {}
816
825
  for plugin in names:
817
- doc = {}
826
+ doc: dict[str, t.Any] = {}
818
827
  try:
819
- doc, plainexamples, returndocs, metadata = get_plugin_docs(plugin, plugin_type, loader, fragment_loader, (context.CLIARGS['verbosity'] > 0))
828
+ doc, plainexamples, returndocs, metadata = self._get_plugin_docs_with_jinja2_builtins(
829
+ plugin,
830
+ plugin_type,
831
+ loader,
832
+ fragment_loader,
833
+ jinja2_builtins,
834
+ )
820
835
  except AnsiblePluginNotFound as e:
821
836
  display.warning(to_native(e))
822
837
  continue
@@ -853,6 +868,39 @@ class DocCLI(CLI, RoleMixin):
853
868
 
854
869
  return plugin_docs
855
870
 
871
+ def _get_plugin_docs_with_jinja2_builtins(
872
+ self,
873
+ plugin_name: str,
874
+ plugin_type: str,
875
+ loader: t.Any,
876
+ fragment_loader: t.Any,
877
+ jinja_builtins: dict[str, str],
878
+ ) -> tuple[dict, str | None, dict | None, dict | None]:
879
+ try:
880
+ return get_plugin_docs(plugin_name, plugin_type, loader, fragment_loader, (context.CLIARGS['verbosity'] > 0))
881
+ except Exception:
882
+ if (desc := jinja_builtins.get(plugin_name, ...)) is not ...:
883
+ short_name = plugin_name.split('.')[-1]
884
+ long_name = f'ansible.builtin.{short_name}'
885
+ # Dynamically build a doc stub for any Jinja2 builtin plugin we haven't
886
+ # explicitly documented.
887
+ doc = dict(
888
+ collection='ansible.builtin',
889
+ plugin_name=long_name,
890
+ filename='',
891
+ short_description=desc,
892
+ description=[
893
+ desc,
894
+ '',
895
+ f"This is the Jinja builtin {plugin_type} plugin {short_name!r}.",
896
+ f"See: U(https://jinja.palletsprojects.com/en/stable/templates/#jinja-{plugin_type}s.{short_name})",
897
+ ],
898
+ )
899
+
900
+ return doc, None, None, None
901
+
902
+ raise
903
+
856
904
  def _get_roles_path(self):
857
905
  """
858
906
  Add any 'roles' subdir in playbook dir to the roles search path.
@@ -1001,10 +1049,10 @@ class DocCLI(CLI, RoleMixin):
1001
1049
  def get_all_plugins_of_type(plugin_type):
1002
1050
  loader = getattr(plugin_loader, '%s_loader' % plugin_type)
1003
1051
  paths = loader._get_paths_with_context()
1004
- plugins = {}
1052
+ plugins = []
1005
1053
  for path_context in paths:
1006
- plugins.update(list_plugins(plugin_type))
1007
- return sorted(plugins.keys())
1054
+ plugins += _list_plugins_with_info(plugin_type).keys()
1055
+ return sorted(plugins)
1008
1056
 
1009
1057
  @staticmethod
1010
1058
  def get_plugin_metadata(plugin_type, plugin_name):
@@ -1101,18 +1149,20 @@ class DocCLI(CLI, RoleMixin):
1101
1149
 
1102
1150
  return text
1103
1151
 
1104
- def _get_plugin_list_descriptions(self, loader):
1152
+ def _get_plugin_list_descriptions(self, plugins: dict[str, _PluginDocMetadata]) -> dict[str, str]:
1105
1153
 
1106
1154
  descs = {}
1107
- for plugin in self.plugins.keys():
1155
+ for plugin, plugin_info in plugins.items():
1108
1156
  # TODO: move to plugin itself i.e: plugin.get_desc()
1109
1157
  doc = None
1110
- filename = Path(to_native(self.plugins[plugin][0]))
1158
+
1111
1159
  docerror = None
1112
- try:
1113
- doc = read_docstub(filename)
1114
- except Exception as e:
1115
- docerror = e
1160
+ if plugin_info.path:
1161
+ filename = Path(to_native(plugin_info.path))
1162
+ try:
1163
+ doc = read_docstub(filename)
1164
+ except Exception as e:
1165
+ docerror = e
1116
1166
 
1117
1167
  # plugin file was empty or had error, lets try other options
1118
1168
  if doc is None:
@@ -1127,9 +1177,15 @@ class DocCLI(CLI, RoleMixin):
1127
1177
  except Exception as e:
1128
1178
  docerror = e
1129
1179
 
1130
- if docerror:
1131
- display.warning("%s has a documentation formatting error: %s" % (plugin, docerror))
1132
- continue
1180
+ # Do a final fallback to see if the plugin is a shadowed Jinja2 plugin
1181
+ # without any explicit documentation.
1182
+ if doc is None and plugin_info.jinja_builtin_short_description:
1183
+ descs[plugin] = plugin_info.jinja_builtin_short_description
1184
+ continue
1185
+
1186
+ if docerror:
1187
+ display.error_as_warning(f"{plugin} has a documentation formatting error.", exception=docerror)
1188
+ continue
1133
1189
 
1134
1190
  if not doc or not isinstance(doc, dict):
1135
1191
  desc = 'UNDOCUMENTED'
@@ -1368,7 +1424,7 @@ class DocCLI(CLI, RoleMixin):
1368
1424
  try:
1369
1425
  text.append(yaml_dump(doc.pop('examples'), indent=2, default_flow_style=False))
1370
1426
  except Exception as e:
1371
- raise AnsibleParserError("Unable to parse examples section", orig_exc=e)
1427
+ raise AnsibleParserError("Unable to parse examples section.") from e
1372
1428
 
1373
1429
  return text
1374
1430
 
@@ -1406,7 +1462,7 @@ class DocCLI(CLI, RoleMixin):
1406
1462
  try:
1407
1463
  text.append('\t' + C.config.get_deprecated_msg_from_config(doc['deprecated'], True, collection_name=collection_name))
1408
1464
  except KeyError as e:
1409
- raise AnsibleError("Invalid deprecation documentation structure", orig_exc=e)
1465
+ raise AnsibleError("Invalid deprecation documentation structure.") from e
1410
1466
  else:
1411
1467
  text.append("%s" % doc['deprecated'])
1412
1468
  del doc['deprecated']
ansible/cli/inventory.py CHANGED
@@ -164,7 +164,7 @@ class InventoryCLI(CLI):
164
164
  import yaml
165
165
  from ansible.parsing.yaml.dumper import AnsibleDumper
166
166
 
167
- # DTFIX-RELEASE: need shared infra to smuggle custom kwargs to dumpers, since yaml.dump cannot (as of PyYAML 6.0.1)
167
+ # DTFIX0: need shared infra to smuggle custom kwargs to dumpers, since yaml.dump cannot (as of PyYAML 6.0.1)
168
168
  dumper = functools.partial(AnsibleDumper, dump_vault_tags=True)
169
169
  results = to_text(yaml.dump(stuff, Dumper=dumper, default_flow_style=False, allow_unicode=True))
170
170
  elif context.CLIARGS['toml']:
@@ -3,17 +3,14 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import sys
6
+ from ansible.utils.display import Display as _Display
7
7
 
8
- HAS_IMPORTLIB_RESOURCES = False
8
+ from importlib.resources import files # pylint: disable=unused-import
9
9
 
10
- if sys.version_info < (3, 10):
11
- try:
12
- from importlib_resources import files # type: ignore[import] # pylint: disable=unused-import
13
- except ImportError:
14
- files = None # type: ignore[assignment]
15
- else:
16
- HAS_IMPORTLIB_RESOURCES = True
17
- else:
18
- from importlib.resources import files
19
- HAS_IMPORTLIB_RESOURCES = True
10
+ HAS_IMPORTLIB_RESOURCES = True
11
+
12
+ _Display().deprecated(
13
+ msg="The `ansible.compat.importlib_resources` module is deprecated.",
14
+ help_text="Use `importlib.resources` from the Python standard library instead.",
15
+ version="2.23",
16
+ )
ansible/config/base.yml CHANGED
@@ -1335,6 +1335,7 @@ DISPLAY_TRACEBACK:
1335
1335
  - error
1336
1336
  - warning
1337
1337
  - deprecated
1338
+ - deprecated_value
1338
1339
  - always
1339
1340
  - never
1340
1341
  version_added: "2.19"
@@ -1961,6 +1962,14 @@ SSH_AGENT:
1961
1962
  env: [{name: ANSIBLE_SSH_AGENT}]
1962
1963
  ini: [{key: ssh_agent, section: connection}]
1963
1964
  version_added: '2.19'
1965
+ SSH_AGENT_EXECUTABLE:
1966
+ name: Executable to start for the ansible-managed SSH agent
1967
+ description: When ``SSH_AGENT`` is ``auto``, the path or name of the ssh agent executable to start.
1968
+ default: ssh-agent
1969
+ type: str
1970
+ env: [ { name: ANSIBLE_SSH_AGENT_EXECUTABLE } ]
1971
+ ini: [ { key: ssh_agent_executable, section: connection } ]
1972
+ version_added: '2.19'
1964
1973
  SSH_AGENT_KEY_LIFETIME:
1965
1974
  name: Set a maximum lifetime when adding identities to an agent
1966
1975
  description: For keys inserted into an agent defined by ``SSH_AGENT``, define a lifetime, in seconds, that the key may remain
@@ -2035,6 +2044,19 @@ TASK_TIMEOUT:
2035
2044
  - {key: task_timeout, section: defaults}
2036
2045
  type: integer
2037
2046
  version_added: '2.10'
2047
+ _TEMPLAR_SANDBOX_MODE:
2048
+ name: Control Jinja template sandbox behavior
2049
+ default: default
2050
+ description:
2051
+ - The default Jinja sandbox behavior blocks template access to all `_` prefixed object attributes and known collection mutation methods (e.g., `dict.clear()`, `list.append()`).
2052
+ type: choices
2053
+ choices:
2054
+ - default
2055
+ - allow_unsafe_attributes
2056
+ env: [{name: _ANSIBLE_TEMPLAR_SANDBOX_MODE}]
2057
+ deprecated:
2058
+ why: controlling sandbox behavior is a temporary workaround
2059
+ version: '2.23'
2038
2060
  _TEMPLAR_UNKNOWN_TYPE_CONVERSION:
2039
2061
  name: Templar unknown type conversion behavior
2040
2062
  default: warning
@@ -3,20 +3,18 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import collections.abc as _c
6
7
  import enum
7
- import traceback
8
- import sys
9
8
  import types
10
9
  import typing as t
11
10
 
12
- from collections.abc import Sequence
13
-
14
11
  from json import JSONDecodeError
15
12
 
16
13
  from ansible.module_utils.common.text.converters import to_text
17
14
  from ..module_utils.datatag import native_type_name
18
15
  from ansible._internal._datatag import _tags
19
- from .._internal._errors import _utils
16
+ from .._internal._errors import _error_utils
17
+ from ansible.module_utils._internal import _text_utils
20
18
 
21
19
  if t.TYPE_CHECKING:
22
20
  from ansible.plugins import loader as _t_loader
@@ -73,7 +71,7 @@ class AnsibleError(Exception):
73
71
  message = str(message)
74
72
 
75
73
  if self._default_message and message:
76
- message = _utils.concat_message(self._default_message, message)
74
+ message = _text_utils.concat_message(self._default_message, message)
77
75
  elif self._default_message:
78
76
  message = self._default_message
79
77
  elif not message:
@@ -108,12 +106,10 @@ class AnsibleError(Exception):
108
106
  @property
109
107
  def message(self) -> str:
110
108
  """
111
- If `include_cause_message` is False, return the original message.
112
- Otherwise, return the original message with cause message(s) appended, stopping on (and including) the first non-AnsibleError.
113
- The recursion is due to `AnsibleError.__str__` calling this method, which uses `str` on child exceptions to create the cause message.
114
- Recursion stops on the first non-AnsibleError since those exceptions do not implement the custom `__str__` behavior.
109
+ Return the original message with cause message(s) appended.
110
+ The cause will not be followed on any `AnsibleError` with `_include_cause_message=False`.
115
111
  """
116
- return _utils.get_chained_message(self)
112
+ return _error_utils.format_exception_message(self)
117
113
 
118
114
  @message.setter
119
115
  def message(self, val) -> None:
@@ -121,8 +117,8 @@ class AnsibleError(Exception):
121
117
 
122
118
  @property
123
119
  def _formatted_source_context(self) -> str | None:
124
- with _utils.RedactAnnotatedSourceContext.when(not self._show_content):
125
- if source_context := _utils.SourceContext.from_value(self.obj):
120
+ with _error_utils.RedactAnnotatedSourceContext.when(not self._show_content):
121
+ if source_context := _error_utils.SourceContext.from_value(self.obj):
126
122
  return str(source_context)
127
123
 
128
124
  return None
@@ -238,8 +234,20 @@ class AnsibleModuleError(AnsibleRuntimeError):
238
234
  """A module failed somehow."""
239
235
 
240
236
 
241
- class AnsibleConnectionFailure(AnsibleRuntimeError):
242
- """The transport / connection_plugin had a fatal error."""
237
+ class AnsibleConnectionFailure(AnsibleRuntimeError, _error_utils.ContributesToTaskResult):
238
+ """
239
+ The transport / connection_plugin had a fatal error.
240
+
241
+ This exception provides a result dictionary via the ContributesToTaskResult mixin.
242
+ """
243
+
244
+ @property
245
+ def result_contribution(self) -> t.Mapping[str, object]:
246
+ return dict(unreachable=True)
247
+
248
+ @property
249
+ def omit_failed_key(self) -> bool:
250
+ return True
243
251
 
244
252
 
245
253
  class AnsibleAuthenticationFailure(AnsibleConnectionFailure):
@@ -319,7 +327,7 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
319
327
  else:
320
328
  message += "Could not find file"
321
329
 
322
- if self.paths and isinstance(self.paths, Sequence):
330
+ if self.paths and isinstance(self.paths, _c.Sequence):
323
331
  searched = to_text('\n\t'.join(self.paths))
324
332
  if message:
325
333
  message += "\n"
@@ -331,47 +339,76 @@ class AnsibleFileNotFound(AnsibleRuntimeError):
331
339
  suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
332
340
 
333
341
 
334
- # These Exceptions are temporary, using them as flow control until we can get a better solution.
335
- # DO NOT USE as they will probably be removed soon.
336
- # We will port the action modules in our tree to use a context manager instead.
337
- class AnsibleAction(AnsibleRuntimeError):
342
+ class AnsibleAction(AnsibleRuntimeError, _error_utils.ContributesToTaskResult):
338
343
  """Base Exception for Action plugin flow control."""
339
344
 
340
345
  def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
341
- super(AnsibleAction, self).__init__(message=message, obj=obj, show_content=show_content,
342
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
343
- if result is None:
344
- self.result = {}
345
- else:
346
- self.result = result
346
+ super().__init__(message=message, obj=obj, show_content=show_content, suppress_extended_error=suppress_extended_error, orig_exc=orig_exc)
347
+
348
+ self._result = result or {}
349
+
350
+ @property
351
+ def result_contribution(self) -> _c.Mapping[str, object]:
352
+ return self._result
353
+
354
+ @property
355
+ def result(self) -> dict[str, object]:
356
+ """Backward compatibility property returning a mutable dictionary."""
357
+ return dict(self.result_contribution)
347
358
 
348
359
 
349
360
  class AnsibleActionSkip(AnsibleAction):
350
- """An action runtime skip."""
361
+ """
362
+ An action runtime skip.
351
363
 
352
- def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
353
- super(AnsibleActionSkip, self).__init__(message=message, obj=obj, show_content=show_content,
354
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
355
- self.result.update({'skipped': True, 'msg': message})
364
+ This exception provides a result dictionary via the ContributesToTaskResult mixin.
365
+ """
366
+
367
+ @property
368
+ def result_contribution(self) -> _c.Mapping[str, object]:
369
+ return self._result | dict(
370
+ skipped=True,
371
+ msg=self.message,
372
+ )
373
+
374
+ @property
375
+ def omit_failed_key(self) -> bool:
376
+ return True
377
+
378
+ @property
379
+ def omit_exception_key(self) -> bool:
380
+ return True
356
381
 
357
382
 
358
383
  class AnsibleActionFail(AnsibleAction):
359
- """An action runtime failure."""
384
+ """
385
+ An action runtime failure.
360
386
 
361
- def __init__(self, message="", obj=None, show_content=True, suppress_extended_error=..., orig_exc=None, result=None):
362
- super(AnsibleActionFail, self).__init__(message=message, obj=obj, show_content=show_content,
363
- suppress_extended_error=suppress_extended_error, orig_exc=orig_exc, result=result)
387
+ This exception provides a result dictionary via the ContributesToTaskResult mixin.
388
+ """
389
+
390
+ @property
391
+ def result_contribution(self) -> _c.Mapping[str, object]:
392
+ return self._result | dict(
393
+ failed=True,
394
+ msg=self.message,
395
+ )
364
396
 
365
- result_overrides = {'failed': True, 'msg': message}
366
- # deprecated: description='use sys.exception()' python_version='3.11'
367
- if sys.exc_info()[1]: # DTFIX-RELEASE: remove this hack once TaskExecutor is no longer shucking AnsibleActionFail and returning its result
368
- result_overrides['exception'] = traceback.format_exc()
369
397
 
370
- self.result.update(result_overrides)
398
+ class _ActionDone(AnsibleAction):
399
+ """
400
+ Imports as `_AnsibleActionDone` are deprecated. An action runtime early exit.
401
+
402
+ This exception provides a result dictionary via the ContributesToTaskResult mixin.
403
+ """
371
404
 
405
+ @property
406
+ def omit_failed_key(self) -> bool:
407
+ return not self._result.get('failed')
372
408
 
373
- class _AnsibleActionDone(AnsibleAction):
374
- """An action runtime early exit."""
409
+ @property
410
+ def omit_exception_key(self) -> bool:
411
+ return not self._result.get('failed')
375
412
 
376
413
 
377
414
  class AnsiblePluginError(AnsibleError):
@@ -422,13 +459,23 @@ def __getattr__(name: str) -> t.Any:
422
459
  """Inject import-time deprecation warnings."""
423
460
  from ..utils.display import Display
424
461
 
425
- if name == 'AnsibleFilterTypeError':
426
- Display().deprecated(
427
- msg="Importing 'AnsibleFilterTypeError' is deprecated.",
428
- help_text=f"Import {AnsibleTypeError.__name__!r} instead.",
429
- version="2.23",
430
- )
462
+ match name:
463
+ case 'AnsibleFilterTypeError':
464
+ Display().deprecated(
465
+ msg=f"Importing {name!r} is deprecated.",
466
+ help_text=f"Import {AnsibleTypeError.__name__!r} instead.",
467
+ version="2.23",
468
+ )
469
+
470
+ return AnsibleTypeError
471
+
472
+ case '_AnsibleActionDone':
473
+ Display().deprecated(
474
+ msg=f"Importing {name!r} is deprecated.",
475
+ help_text="Return directly from action plugins instead.",
476
+ version="2.23",
477
+ )
431
478
 
432
- return AnsibleTypeError
479
+ return _ActionDone
433
480
 
434
481
  raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
@@ -40,6 +40,7 @@ from ansible._internal import _locking
40
40
  from ansible._internal._datatag import _utils
41
41
  from ansible.module_utils._internal import _dataclass_validation
42
42
  from ansible.module_utils.common.yaml import yaml_load
43
+ from ansible.module_utils.datatag import deprecator_from_collection_name
43
44
  from ansible._internal._datatag._tags import Origin
44
45
  from ansible.module_utils.common.json import Direction, get_module_encoder
45
46
  from ansible.release import __version__, __author__
@@ -55,7 +56,6 @@ from ansible.template import Templar
55
56
  from ansible.utils.collection_loader._collection_finder import _get_collection_metadata, _nested_dict_get
56
57
  from ansible.module_utils._internal import _json, _ansiballz
57
58
  from ansible.module_utils import basic as _basic
58
- from ansible.module_utils.common import messages as _messages
59
59
 
60
60
  if t.TYPE_CHECKING:
61
61
  from ansible import template as _template
@@ -166,7 +166,7 @@ NEW_STYLE_PYTHON_MODULE_RE = re.compile(
166
166
 
167
167
 
168
168
  class ModuleDepFinder(ast.NodeVisitor):
169
- # DTFIX-RELEASE: add support for ignoring imports with a "controller only" comment, this will allow replacing import_controller_module with standard imports
169
+ # DTFIX-FUTURE: add support for ignoring imports with a "controller only" comment, this will allow replacing import_controller_module with standard imports
170
170
  def __init__(self, module_fqn, tree, is_pkg_init=False, *args, **kwargs):
171
171
  """
172
172
  Walk the ast tree for the python module.
@@ -439,7 +439,7 @@ class ModuleUtilLocatorBase:
439
439
  version=removal_version,
440
440
  removed=removed,
441
441
  date=removal_date,
442
- deprecator=_messages.PluginInfo._from_collection_name(self._collection_name),
442
+ deprecator=deprecator_from_collection_name(self._collection_name),
443
443
  )
444
444
  if 'redirect' in routing_entry:
445
445
  self.redirected = True
@@ -618,7 +618,7 @@ class CollectionModuleUtilLocator(ModuleUtilLocatorBase):
618
618
  if pkg_path:
619
619
  origin = Origin(path=os.path.join(pkg_path, src_path))
620
620
  else:
621
- # DTFIX-RELEASE: not sure if this case is even reachable
621
+ # DTFIX-FUTURE: not sure if this case is even reachable
622
622
  origin = Origin(description=f'<synthetic collection package for {collection_pkg_name}!r>')
623
623
 
624
624
  self.source_code = origin.tag(src)
@@ -658,7 +658,7 @@ metadata_versions: dict[t.Any, type[ModuleMetadata]] = {
658
658
 
659
659
 
660
660
  def _get_module_metadata(module: ast.Module) -> ModuleMetadata:
661
- # DTFIX-RELEASE: while module metadata works, this feature isn't fully baked and should be turned off before release
661
+ # DTFIX2: while module metadata works, this feature isn't fully baked and should be turned off before release
662
662
  metadata_nodes: list[ast.Assign] = []
663
663
 
664
664
  for node in module.body:
@@ -928,7 +928,7 @@ class _BuiltModule:
928
928
  class _CachedModule:
929
929
  """Cached Python module created by AnsiballZ."""
930
930
 
931
- # DTFIX-RELEASE: secure this (locked down pickle, don't use pickle, etc.)
931
+ # DTFIX5: secure this (locked down pickle, don't use pickle, etc.)
932
932
 
933
933
  zip_data: bytes
934
934
  metadata: ModuleMetadata
@@ -991,10 +991,8 @@ def _find_module_utils(
991
991
  module_substyle = 'powershell'
992
992
  b_module_data = b_module_data.replace(REPLACER_WINDOWS, b'#AnsibleRequires -PowerShell Ansible.ModuleUtils.Legacy')
993
993
  elif re.search(b'#Requires -Module', b_module_data, re.IGNORECASE) \
994
- or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE)\
995
- or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE) \
996
- or re.search(b'#AnsibleRequires -Powershell', b_module_data, re.IGNORECASE) \
997
- or re.search(b'#AnsibleRequires -CSharpUtil', b_module_data, re.IGNORECASE):
994
+ or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE) \
995
+ or re.search(b'#AnsibleRequires -(OSVersion|PowerShell|CSharpUtil|Wrapper)', b_module_data, re.IGNORECASE):
998
996
  module_style = 'new'
999
997
  module_substyle = 'powershell'
1000
998
  elif REPLACER_JSONARGS in b_module_data:
@@ -40,7 +40,7 @@ param([ScriptBlock]$ScriptBlock, $Param)
40
40
  & $ScriptBlock.Ast.GetScriptBlock() @Param
41
41
  '@).AddParameters(
42
42
  @{
43
- ScriptBlock = $execInfo.ScriptBlock
43
+ ScriptBlock = $execInfo.ScriptInfo.ScriptBlock
44
44
  Param = $execInfo.Parameters
45
45
  })
46
46
 
@@ -64,7 +64,7 @@ $jobError = $null
64
64
  try {
65
65
  $jobAsyncResult = $ps.BeginInvoke($pipelineInput, $invocationSettings, $null, $null)
66
66
  $jobAsyncResult.AsyncWaitHandle.WaitOne($Timeout * 1000) > $null
67
- $result.finished = 1
67
+ $result.finished = $true
68
68
 
69
69
  if ($jobAsyncResult.IsCompleted) {
70
70
  $jobOutput = $ps.EndInvoke($jobAsyncResult)
@@ -113,7 +113,7 @@ try {
113
113
  }
114
114
  $execWrapper = @{
115
115
  name = 'exec_wrapper-async.ps1'
116
- script = $execAction.Script
116
+ script = $execAction.ScriptInfo.Script
117
117
  params = $execAction.Parameters
118
118
  } | ConvertTo-Json -Compress -Depth 99
119
119
  $asyncInput = "$execWrapper`n`0`0`0`0`n$($execAction.InputData)"
@@ -135,8 +135,8 @@ try {
135
135
  # We need to write the result file before the process is started to ensure
136
136
  # it can read the file.
137
137
  $result = @{
138
- started = 1
139
- finished = 0
138
+ started = $true
139
+ finished = $false
140
140
  results_file = $resultsPath
141
141
  ansible_job_id = $localJid
142
142
  _ansible_suppress_tmpdir_delete = $true
@@ -7,6 +7,7 @@ using namespace System.Collections
7
7
  using namespace System.Diagnostics
8
8
  using namespace System.IO
9
9
  using namespace System.Management.Automation
10
+ using namespace System.Management.Automation.Security
10
11
  using namespace System.Net
11
12
  using namespace System.Text
12
13
 
@@ -53,7 +54,7 @@ $executablePath = Join-Path -Path $PSHome -ChildPath $executable
53
54
  $actionInfo = Get-AnsibleExecWrapper -EncodeInputOutput
54
55
  $bootstrapManifest = ConvertTo-Json -InputObject @{
55
56
  n = "exec_wrapper-become-$([Guid]::NewGuid()).ps1"
56
- s = $actionInfo.Script
57
+ s = $actionInfo.ScriptInfo.Script
57
58
  p = $actionInfo.Parameters
58
59
  } -Depth 99 -Compress
59
60
 
@@ -68,9 +69,26 @@ $m=foreach($i in $input){
68
69
  $m=$m|ConvertFrom-Json
69
70
  $p=@{}
70
71
  foreach($o in $m.p.PSObject.Properties){$p[$o.Name]=$o.Value}
72
+ '@
73
+
74
+ if ([SystemPolicy]::GetSystemLockdownPolicy() -eq 'Enforce') {
75
+ # If we started in CLM we need to execute the script from a file so that
76
+ # PowerShell validates our exec_wrapper is trusted and will run in FLM.
77
+ $command += @'
78
+ $n=Join-Path $env:TEMP $m.n
79
+ $null=New-Item $n -Value $m.s -Type File -Force
80
+ try{$input|&$n @p}
81
+ finally{if(Test-Path -LiteralPath $n){Remove-Item -LiteralPath $n -Force}}
82
+ '@
83
+ }
84
+ else {
85
+ # If we started in FLM we pass the script through stdin and execute in
86
+ # memory.
87
+ $command += @'
71
88
  $c=[System.Management.Automation.Language.Parser]::ParseInput($m.s,$m.n,[ref]$null,[ref]$null).GetScriptBlock()
72
- $input | & $c @p
89
+ $input|&$c @p
73
90
  '@
91
+ }
74
92
 
75
93
  # Strip out any leading or trailing whitespace and remove empty lines.
76
94
  $command = @(