ansible-core 2.19.0b3__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 (174) hide show
  1. ansible/_internal/__init__.py +2 -2
  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 +6 -6
  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/_testing.py +26 -0
  26. ansible/_internal/_yaml/_dumper.py +1 -1
  27. ansible/_internal/_yaml/_errors.py +7 -7
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  29. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  30. ansible/cli/__init__.py +5 -82
  31. ansible/cli/arguments/option_helpers.py +8 -5
  32. ansible/cli/doc.py +84 -28
  33. ansible/cli/inventory.py +1 -1
  34. ansible/compat/importlib_resources.py +9 -12
  35. ansible/config/base.yml +27 -23
  36. ansible/config/manager.py +142 -101
  37. ansible/constants.py +1 -1
  38. ansible/errors/__init__.py +96 -49
  39. ansible/executor/module_common.py +8 -10
  40. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  41. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  42. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  43. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  44. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  45. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  46. ansible/executor/powershell/module_manifest.py +52 -0
  47. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  48. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  49. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  50. ansible/executor/process/worker.py +38 -113
  51. ansible/executor/task_executor.py +26 -61
  52. ansible/executor/task_result.py +2 -4
  53. ansible/galaxy/collection/__init__.py +1 -4
  54. ansible/inventory/manager.py +1 -0
  55. ansible/module_utils/_internal/__init__.py +0 -3
  56. ansible/module_utils/_internal/_ambient_context.py +3 -3
  57. ansible/module_utils/_internal/_ansiballz.py +4 -2
  58. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  59. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  60. ansible/module_utils/_internal/_deprecator.py +66 -48
  61. ansible/module_utils/_internal/_errors.py +88 -17
  62. ansible/module_utils/_internal/_event_utils.py +61 -0
  63. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  64. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  65. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  66. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  67. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  68. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  69. ansible/module_utils/_internal/_plugin_info.py +1 -1
  70. ansible/module_utils/_internal/_stack.py +22 -0
  71. ansible/module_utils/_internal/_text_utils.py +6 -0
  72. ansible/module_utils/_internal/_traceback.py +11 -8
  73. ansible/module_utils/ansible_release.py +1 -1
  74. ansible/module_utils/basic.py +49 -15
  75. ansible/module_utils/common/arg_spec.py +2 -2
  76. ansible/module_utils/common/collections.py +6 -0
  77. ansible/module_utils/common/json.py +2 -2
  78. ansible/module_utils/common/text/converters.py +3 -3
  79. ansible/module_utils/common/validation.py +1 -1
  80. ansible/module_utils/common/warnings.py +80 -23
  81. ansible/module_utils/common/yaml.py +1 -1
  82. ansible/module_utils/datatag.py +5 -2
  83. ansible/module_utils/facts/system/distribution.py +16 -3
  84. ansible/module_utils/facts/virtual/linux.py +2 -2
  85. ansible/module_utils/parsing/convert_bool.py +6 -0
  86. ansible/module_utils/service.py +2 -9
  87. ansible/modules/apt_repository.py +7 -29
  88. ansible/modules/assemble.py +4 -4
  89. ansible/modules/async_status.py +13 -11
  90. ansible/modules/async_wrapper.py +5 -5
  91. ansible/modules/cron.py +3 -5
  92. ansible/modules/dnf5.py +15 -22
  93. ansible/modules/git.py +1 -6
  94. ansible/modules/hostname.py +0 -1
  95. ansible/modules/pip.py +2 -4
  96. ansible/modules/service.py +3 -9
  97. ansible/modules/sysvinit.py +3 -3
  98. ansible/parsing/ajson.py +3 -5
  99. ansible/parsing/dataloader.py +4 -4
  100. ansible/parsing/mod_args.py +1 -1
  101. ansible/parsing/plugin_docs.py +2 -2
  102. ansible/parsing/utils/yaml.py +3 -3
  103. ansible/parsing/vault/__init__.py +4 -4
  104. ansible/playbook/playbook_include.py +1 -1
  105. ansible/playbook/taggable.py +0 -3
  106. ansible/plugins/__init__.py +0 -25
  107. ansible/plugins/action/__init__.py +9 -32
  108. ansible/plugins/action/add_host.py +1 -1
  109. ansible/plugins/action/assemble.py +8 -16
  110. ansible/plugins/action/async_status.py +7 -2
  111. ansible/plugins/action/copy.py +8 -7
  112. ansible/plugins/action/gather_facts.py +8 -8
  113. ansible/plugins/action/package.py +5 -8
  114. ansible/plugins/action/script.py +8 -15
  115. ansible/plugins/action/service.py +3 -7
  116. ansible/plugins/action/template.py +6 -8
  117. ansible/plugins/action/unarchive.py +5 -15
  118. ansible/plugins/action/uri.py +9 -20
  119. ansible/plugins/callback/__init__.py +4 -6
  120. ansible/plugins/callback/junit.py +4 -2
  121. ansible/plugins/connection/local.py +2 -2
  122. ansible/plugins/connection/ssh.py +17 -9
  123. ansible/plugins/connection/winrm.py +5 -2
  124. ansible/plugins/doc_fragments/constructed.py +2 -2
  125. ansible/plugins/filter/core.py +13 -6
  126. ansible/plugins/filter/encryption.py +4 -4
  127. ansible/plugins/inventory/__init__.py +11 -10
  128. ansible/plugins/inventory/script.py +1 -1
  129. ansible/plugins/list.py +69 -16
  130. ansible/plugins/loader.py +10 -9
  131. ansible/plugins/lookup/csvfile.py +16 -71
  132. ansible/plugins/lookup/first_found.py +2 -1
  133. ansible/plugins/shell/__init__.py +56 -2
  134. ansible/plugins/shell/powershell.py +66 -9
  135. ansible/plugins/shell/sh.py +9 -5
  136. ansible/plugins/test/core.py +21 -15
  137. ansible/plugins/test/finished.yml +1 -1
  138. ansible/plugins/test/uri.py +2 -5
  139. ansible/release.py +1 -1
  140. ansible/template/__init__.py +30 -2
  141. ansible/utils/collection_loader/__init__.py +2 -0
  142. ansible/utils/display.py +107 -128
  143. ansible/utils/hashing.py +0 -1
  144. ansible/utils/listify.py +6 -4
  145. ansible/utils/plugin_docs.py +2 -1
  146. ansible/utils/unsafe_proxy.py +1 -1
  147. ansible/vars/hostvars.py +1 -1
  148. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
  149. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
  150. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  151. ansible_test/_data/completion/docker.txt +3 -3
  152. ansible_test/_data/completion/remote.txt +1 -0
  153. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  154. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  155. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  156. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  157. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  158. ansible_test/_internal/util.py +20 -0
  159. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  160. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  161. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  162. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  163. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  164. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
  165. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  166. ansible/_internal/_errors/_utils.py +0 -310
  167. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  168. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
  169. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
  170. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
  171. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
  172. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
  173. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
  174. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  """Internal utilities for serialization and deserialization."""
2
2
 
3
- # DTFIX-RELEASE: most of this isn't JSON specific, find a better home
3
+ # DTFIX-FUTURE: most of this isn't JSON specific, find a better home
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -144,20 +144,20 @@ class AnsibleVariableVisitor:
144
144
  value = self._template_engine.transform(value)
145
145
  value_type = type(value)
146
146
 
147
- # DTFIX-RELEASE: need to handle native copy for keys too
147
+ # DTFIX3: need to handle native copy for keys too
148
148
  if self.convert_to_native_values and isinstance(value, _datatag.AnsibleTaggedObject):
149
149
  value = value._native_copy()
150
150
  value_type = type(value)
151
151
 
152
152
  result: _T
153
153
 
154
- # DTFIX-RELEASE: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
155
- # keep in mind the allowed types for keys is a more restrictive set than for values (str and taggged str only, not EncryptedString)
156
- # DTFIX-RELEASE: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
154
+ # DTFIX3: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
155
+ # keep in mind the allowed types for keys is a more restrictive set than for values (str and tagged str only, not EncryptedString)
156
+ # DTFIX5: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
157
157
 
158
158
  if (result := self._early_visit(value, value_type)) is not _sentinel:
159
159
  pass
160
- # DTFIX-RELEASE: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
160
+ # DTFIX7: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
161
161
  elif value_type in _ANSIBLE_ALLOWED_MAPPING_VAR_TYPES: # check mappings first, because they're also collections
162
162
  with self: # supports StateTrackingMixIn
163
163
  result = AnsibleTagHelper.tag_copy(value, ((k, self._visit(k, v)) for k, v in value.items()), value_type=value_type)
@@ -46,6 +46,8 @@ class _Profile(_profiles._JSONSerializationProfile):
46
46
  _datetime.datetime: _datatag.AnsibleSerializableDateTime,
47
47
  }
48
48
 
49
+ cls.handle_key = cls._handle_key_str_fallback # legacy stdlib-compatible key behavior
50
+
49
51
 
50
52
  class Encoder(_profiles.AnsibleProfileJSONEncoder):
51
53
  _profile = _Profile
@@ -12,7 +12,7 @@ from . import _legacy
12
12
  class _InventoryVariableVisitor(_legacy._LegacyVariableVisitor, _json.StateTrackingMixIn):
13
13
  """State-tracking visitor implementation that only applies trust to `_meta.hostvars` and `vars` inventory values."""
14
14
 
15
- # DTFIX-RELEASE: does the variable visitor need to support conversion of sequence/mapping for inventory?
15
+ # DTFIX5: does the variable visitor need to support conversion of sequence/mapping for inventory?
16
16
 
17
17
  @property
18
18
  def _allow_trust(self) -> bool:
@@ -152,9 +152,11 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
152
152
  '__ansible_vault': cls.deserialize_vault,
153
153
  }
154
154
 
155
+ cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
156
+
155
157
  @classmethod
156
158
  def pre_serialize(cls, encoder: Encoder, o: _t.Any) -> _t.Any:
157
- # DTFIX-RELEASE: these conversion args probably aren't needed
159
+ # DTFIX7: these conversion args probably aren't needed
158
160
  avv = cls.visitor_type(invert_trust=True, convert_mapping_to_dict=True, convert_sequence_to_list=True, convert_custom_scalars=True)
159
161
 
160
162
  return avv.visit(o)
@@ -165,16 +167,6 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
165
167
 
166
168
  return avv.visit(o)
167
169
 
168
- @classmethod
169
- def handle_key(cls, k: _t.Any) -> _t.Any:
170
- if isinstance(k, str):
171
- return k
172
-
173
- # DTFIX-RELEASE: decide if this is a deprecation warning, error, or what?
174
- # Non-string variable names have been disallowed by set_fact and other things since at least 2021.
175
- # DTFIX-RELEASE: document why this behavior is here, also verify the legacy tagless use case doesn't need this same behavior
176
- return str(k)
177
-
178
170
 
179
171
  class Encoder(_profiles.AnsibleProfileJSONEncoder):
180
172
  _profile = _Profile
File without changes
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import os
5
+ import subprocess
6
+
7
+ from ansible import constants as C
8
+ from ansible._internal._errors import _alarm_timeout
9
+ from ansible._internal._ssh._ssh_agent import SshAgentClient
10
+ from ansible.cli import display
11
+ from ansible.errors import AnsibleError
12
+ from ansible.module_utils.common.process import get_bin_path
13
+
14
+ _SSH_AGENT_STDOUT_READ_TIMEOUT = 5 # seconds
15
+
16
+
17
+ def launch_ssh_agent() -> None:
18
+ """If configured via `SSH_AGENT`, launch an ssh-agent for Ansible's use and/or verify access to an existing one."""
19
+ try:
20
+ _launch_ssh_agent()
21
+ except Exception as ex:
22
+ raise AnsibleError("Failed to launch ssh agent.") from ex
23
+
24
+
25
+ def _launch_ssh_agent() -> None:
26
+ ssh_agent_cfg = C.config.get_config_value('SSH_AGENT')
27
+
28
+ match ssh_agent_cfg:
29
+ case 'none':
30
+ display.debug('SSH_AGENT set to none')
31
+ return
32
+ case 'auto':
33
+ try:
34
+ ssh_agent_bin = get_bin_path(C.config.get_config_value('SSH_AGENT_EXECUTABLE'))
35
+ except ValueError as e:
36
+ raise AnsibleError('SSH_AGENT set to auto, but cannot find ssh-agent binary.') from e
37
+
38
+ ssh_agent_dir = os.path.join(C.DEFAULT_LOCAL_TMP, 'ssh_agent')
39
+ os.mkdir(ssh_agent_dir, 0o700)
40
+ sock = os.path.join(ssh_agent_dir, 'agent.sock')
41
+ display.vvv('SSH_AGENT: starting...')
42
+
43
+ try:
44
+ p = subprocess.Popen(
45
+ [ssh_agent_bin, '-D', '-s', '-a', sock],
46
+ stdin=subprocess.PIPE,
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.PIPE,
49
+ text=True,
50
+ )
51
+ except OSError as e:
52
+ raise AnsibleError('Could not start ssh-agent.') from e
53
+
54
+ atexit.register(p.terminate)
55
+
56
+ help_text = f'The ssh-agent {ssh_agent_bin!r} might be an incompatible agent.'
57
+ expected_stdout = 'SSH_AUTH_SOCK'
58
+
59
+ try:
60
+ with _alarm_timeout.AnsibleTimeoutError.alarm_timeout(_SSH_AGENT_STDOUT_READ_TIMEOUT):
61
+ stdout = p.stdout.read(len(expected_stdout))
62
+ except _alarm_timeout.AnsibleTimeoutError as e:
63
+ display.error_as_warning(
64
+ msg=f'Timed out waiting for expected stdout {expected_stdout!r} from ssh-agent.',
65
+ exception=e,
66
+ help_text=help_text,
67
+ )
68
+ else:
69
+ if stdout != expected_stdout:
70
+ display.warning(
71
+ msg=f'The ssh-agent output {stdout!r} did not match expected {expected_stdout!r}.',
72
+ help_text=help_text,
73
+ )
74
+
75
+ if p.poll() is not None:
76
+ raise AnsibleError(
77
+ message='The ssh-agent terminated prematurely.',
78
+ help_text=f'{help_text}\n\nReturn Code: {p.returncode}\nStandard Error:\n{p.stderr.read()}',
79
+ )
80
+
81
+ display.vvv(f'SSH_AGENT: ssh-agent[{p.pid}] started and bound to {sock}')
82
+ case _:
83
+ sock = ssh_agent_cfg
84
+
85
+ try:
86
+ with SshAgentClient(sock) as client:
87
+ client.list()
88
+ except Exception as e:
89
+ raise AnsibleError(f'Could not communicate with ssh-agent using auth sock {sock!r}.') from e
90
+
91
+ os.environ['SSH_AUTH_SOCK'] = os.environ['ANSIBLE_SSH_AGENT'] = sock
@@ -106,21 +106,19 @@ class SshAgentFailure(RuntimeError):
106
106
  # NOTE: Classes below somewhat represent "Data Type Representations Used in the SSH Protocols"
107
107
  # as specified by RFC4251
108
108
 
109
+
109
110
  @t.runtime_checkable
110
111
  class SupportsToBlob(t.Protocol):
111
- def to_blob(self) -> bytes:
112
- ...
112
+ def to_blob(self) -> bytes: ...
113
113
 
114
114
 
115
115
  @t.runtime_checkable
116
116
  class SupportsFromBlob(t.Protocol):
117
117
  @classmethod
118
- def from_blob(cls, blob: memoryview | bytes) -> t.Self:
119
- ...
118
+ def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
120
119
 
121
120
  @classmethod
122
- def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
123
- ...
121
+ def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]: ...
124
122
 
125
123
 
126
124
  def _split_blob(blob: memoryview | bytes, length: int) -> tuple[memoryview | bytes, memoryview | bytes]:
@@ -304,10 +302,12 @@ class PrivateKeyMsg(Msg):
304
302
  return EcdsaPrivateKeyMsg(
305
303
  getattr(KeyAlgo, f'ECDSA{key_size}'),
306
304
  unicode_string(f'nistp{key_size}'),
307
- binary_string(private_key.public_key().public_bytes(
308
- encoding=serialization.Encoding.X962,
309
- format=serialization.PublicFormat.UncompressedPoint
310
- )),
305
+ binary_string(
306
+ private_key.public_key().public_bytes(
307
+ encoding=serialization.Encoding.X962,
308
+ format=serialization.PublicFormat.UncompressedPoint,
309
+ )
310
+ ),
311
311
  mpint(ecdsa_pn.private_value),
312
312
  )
313
313
  case Ed25519PrivateKey():
@@ -318,7 +318,7 @@ class PrivateKeyMsg(Msg):
318
318
  private_bytes = private_key.private_bytes(
319
319
  encoding=serialization.Encoding.Raw,
320
320
  format=serialization.PrivateFormat.Raw,
321
- encryption_algorithm=serialization.NoEncryption()
321
+ encryption_algorithm=serialization.NoEncryption(),
322
322
  )
323
323
  return Ed25519PrivateKeyMsg(
324
324
  KeyAlgo.ED25519,
@@ -376,14 +376,14 @@ class Ed25519PrivateKeyMsg(PrivateKeyMsg):
376
376
  @dataclasses.dataclass
377
377
  class PublicKeyMsg(Msg):
378
378
  @staticmethod
379
- def get_dataclass(
380
- type: KeyAlgo
381
- ) -> type[t.Union[
379
+ def get_dataclass(type: KeyAlgo) -> type[
380
+ t.Union[
382
381
  RSAPublicKeyMsg,
383
382
  EcdsaPublicKeyMsg,
384
383
  Ed25519PublicKeyMsg,
385
- DSAPublicKeyMsg
386
- ]]:
384
+ DSAPublicKeyMsg,
385
+ ]
386
+ ]:
387
387
  match type:
388
388
  case KeyAlgo.RSA:
389
389
  return RSAPublicKeyMsg
@@ -401,29 +401,14 @@ class PublicKeyMsg(Msg):
401
401
  type: KeyAlgo = self.type
402
402
  match type:
403
403
  case KeyAlgo.RSA:
404
- return RSAPublicNumbers(
405
- self.e,
406
- self.n
407
- ).public_key()
404
+ return RSAPublicNumbers(self.e, self.n).public_key()
408
405
  case KeyAlgo.ECDSA256 | KeyAlgo.ECDSA384 | KeyAlgo.ECDSA521:
409
406
  curve = _ECDSA_KEY_TYPE[KeyAlgo(type)]
410
- return EllipticCurvePublicKey.from_encoded_point(
411
- curve(),
412
- self.Q
413
- )
407
+ return EllipticCurvePublicKey.from_encoded_point(curve(), self.Q)
414
408
  case KeyAlgo.ED25519:
415
- return Ed25519PublicKey.from_public_bytes(
416
- self.enc_a
417
- )
409
+ return Ed25519PublicKey.from_public_bytes(self.enc_a)
418
410
  case KeyAlgo.DSA:
419
- return DSAPublicNumbers(
420
- self.y,
421
- DSAParameterNumbers(
422
- self.p,
423
- self.q,
424
- self.g
425
- )
426
- ).public_key()
411
+ return DSAPublicNumbers(self.y, DSAParameterNumbers(self.p, self.q, self.g)).public_key()
427
412
  case _:
428
413
  raise NotImplementedError(type)
429
414
 
@@ -437,32 +422,32 @@ class PublicKeyMsg(Msg):
437
422
  mpint(dsa_pn.parameter_numbers.p),
438
423
  mpint(dsa_pn.parameter_numbers.q),
439
424
  mpint(dsa_pn.parameter_numbers.g),
440
- mpint(dsa_pn.y)
425
+ mpint(dsa_pn.y),
441
426
  )
442
427
  case EllipticCurvePublicKey():
443
428
  return EcdsaPublicKeyMsg(
444
429
  getattr(KeyAlgo, f'ECDSA{public_key.curve.key_size}'),
445
430
  unicode_string(f'nistp{public_key.curve.key_size}'),
446
- binary_string(public_key.public_bytes(
447
- encoding=serialization.Encoding.X962,
448
- format=serialization.PublicFormat.UncompressedPoint
449
- ))
431
+ binary_string(
432
+ public_key.public_bytes(
433
+ encoding=serialization.Encoding.X962,
434
+ format=serialization.PublicFormat.UncompressedPoint,
435
+ )
436
+ ),
450
437
  )
451
438
  case Ed25519PublicKey():
452
439
  return Ed25519PublicKeyMsg(
453
440
  KeyAlgo.ED25519,
454
- binary_string(public_key.public_bytes(
455
- encoding=serialization.Encoding.Raw,
456
- format=serialization.PublicFormat.Raw,
457
- ))
441
+ binary_string(
442
+ public_key.public_bytes(
443
+ encoding=serialization.Encoding.Raw,
444
+ format=serialization.PublicFormat.Raw,
445
+ )
446
+ ),
458
447
  )
459
448
  case RSAPublicKey():
460
449
  rsa_pn: RSAPublicNumbers = public_key.public_numbers()
461
- return RSAPublicKeyMsg(
462
- KeyAlgo.RSA,
463
- mpint(rsa_pn.e),
464
- mpint(rsa_pn.n)
465
- )
450
+ return RSAPublicKeyMsg(KeyAlgo.RSA, mpint(rsa_pn.e), mpint(rsa_pn.n))
466
451
  case _:
467
452
  raise NotImplementedError(public_key)
468
453
 
@@ -473,10 +458,7 @@ class PublicKeyMsg(Msg):
473
458
  msg.comments = unicode_string('')
474
459
  k = msg.to_blob()
475
460
  digest.update(k)
476
- return binascii.b2a_base64(
477
- digest.digest(),
478
- newline=False
479
- ).rstrip(b'=').decode('utf-8')
461
+ return binascii.b2a_base64(digest.digest(), newline=False).rstrip(b'=').decode('utf-8')
480
462
 
481
463
 
482
464
  @dataclasses.dataclass(order=True, slots=True)
@@ -519,9 +501,7 @@ class KeyList(Msg):
519
501
 
520
502
  def __post_init__(self) -> None:
521
503
  if self.nkeys != len(self.keys):
522
- raise SshAgentFailure(
523
- "agent: invalid number of keys received for identities list"
524
- )
504
+ raise SshAgentFailure("agent: invalid number of keys received for identities list")
525
505
 
526
506
 
527
507
  @dataclasses.dataclass(order=True, slots=True)
@@ -535,8 +515,7 @@ class PublicKeyMsgList(Msg):
535
515
  return len(self.keys)
536
516
 
537
517
  @classmethod
538
- def from_blob(cls, blob: memoryview | bytes) -> t.Self:
539
- ...
518
+ def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
540
519
 
541
520
  @classmethod
542
521
  def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
@@ -546,22 +525,16 @@ class PublicKeyMsgList(Msg):
546
525
  key_blob, key_blob_length, comment_blob = cls._consume_field(blob)
547
526
 
548
527
  peek_key_algo, _length, _blob = cls._consume_field(key_blob)
549
- pub_key_msg_cls = PublicKeyMsg.get_dataclass(
550
- KeyAlgo(bytes(peek_key_algo).decode('utf-8'))
551
- )
528
+ pub_key_msg_cls = PublicKeyMsg.get_dataclass(KeyAlgo(bytes(peek_key_algo).decode('utf-8')))
552
529
 
553
530
  _fv, comment_blob_length, blob = cls._consume_field(comment_blob)
554
- key_plus_comment = (
555
- prev_blob[4: (4 + key_blob_length) + (4 + comment_blob_length)]
556
- )
531
+ key_plus_comment = prev_blob[4 : (4 + key_blob_length) + (4 + comment_blob_length)]
557
532
 
558
533
  args.append(pub_key_msg_cls.from_blob(key_plus_comment))
559
534
  return cls(args), b""
560
535
 
561
536
  @staticmethod
562
- def _consume_field(
563
- blob: memoryview | bytes
564
- ) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
537
+ def _consume_field(blob: memoryview | bytes) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
565
538
  length = uint32.from_blob(blob[:4])
566
539
  blob = blob[4:]
567
540
  data, rest = _split_blob(blob, length)
@@ -581,10 +554,10 @@ class SshAgentClient:
581
554
  return self
582
555
 
583
556
  def __exit__(
584
- self,
585
- exc_type: type[BaseException] | None,
586
- exc_value: BaseException | None,
587
- traceback: types.TracebackType | None
557
+ self,
558
+ exc_type: type[BaseException] | None,
559
+ exc_value: BaseException | None,
560
+ traceback: types.TracebackType | None,
588
561
  ) -> None:
589
562
  self.close()
590
563
 
@@ -598,34 +571,25 @@ class SshAgentClient:
598
571
  return resp
599
572
 
600
573
  def remove_all(self) -> None:
601
- self.send(
602
- ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob()
603
- )
574
+ self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob())
604
575
 
605
576
  def remove(self, public_key: CryptoPublicKey) -> None:
606
577
  key_blob = PublicKeyMsg.from_public_key(public_key).to_blob()
607
- self.send(
608
- ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() +
609
- uint32(len(key_blob)).to_blob() + key_blob
610
- )
578
+ self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() + uint32(len(key_blob)).to_blob() + key_blob)
611
579
 
612
580
  def add(
613
- self,
614
- private_key: CryptoPrivateKey,
615
- comments: str | None = None,
616
- lifetime: int | None = None,
617
- confirm: bool | None = None,
581
+ self,
582
+ private_key: CryptoPrivateKey,
583
+ comments: str | None = None,
584
+ lifetime: int | None = None,
585
+ confirm: bool | None = None,
618
586
  ) -> None:
619
587
  key_msg = PrivateKeyMsg.from_private_key(private_key)
620
588
  key_msg.comments = unicode_string(comments or '')
621
589
  if lifetime:
622
- key_msg.constraints += constraints(
623
- [ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]
624
- ).to_blob() + uint32(lifetime).to_blob()
590
+ key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]).to_blob() + uint32(lifetime).to_blob()
625
591
  if confirm:
626
- key_msg.constraints += constraints(
627
- [ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]
628
- ).to_blob()
592
+ key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]).to_blob()
629
593
 
630
594
  if key_msg.constraints:
631
595
  msg = ProtocolMsgNumbers.SSH_AGENTC_ADD_ID_CONSTRAINED.to_blob()
@@ -638,9 +602,7 @@ class SshAgentClient:
638
602
  req = ProtocolMsgNumbers.SSH_AGENTC_REQUEST_IDENTITIES.to_blob()
639
603
  r = memoryview(bytearray(self.send(req)))
640
604
  if r[0] != ProtocolMsgNumbers.SSH_AGENT_IDENTITIES_ANSWER:
641
- raise SshAgentFailure(
642
- 'agent: non-identities answer received for identities list'
643
- )
605
+ raise SshAgentFailure('agent: non-identities answer received for identities list')
644
606
  return KeyList.from_blob(r[1:])
645
607
 
646
608
  def __contains__(self, public_key: CryptoPublicKey) -> bool:
@@ -649,7 +611,7 @@ class SshAgentClient:
649
611
 
650
612
 
651
613
  @functools.cache
652
- def _key_data_into_crypto_objects(key_data: bytes, passphrase: bytes | None) -> tuple[CryptoPrivateKey, CryptoPublicKey, str]:
614
+ def key_data_into_crypto_objects(key_data: bytes, passphrase: bytes | None) -> tuple[CryptoPrivateKey, CryptoPublicKey, str]:
653
615
  private_key = serialization.ssh.load_ssh_private_key(key_data, passphrase)
654
616
  public_key = private_key.public_key()
655
617
  fingerprint = PublicKeyMsg.from_public_key(public_key).fingerprint
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from jinja2 import __version__ as _jinja2_version
3
+ import importlib.metadata
4
+
5
+ jinja2_version = importlib.metadata.version('jinja2')
4
6
 
5
7
  # DTFIX-FUTURE: sanity test to ensure this doesn't drift from requirements
6
8
  _MINIMUM_JINJA_VERSION = (3, 1)
7
- _CURRENT_JINJA_VERSION = tuple(map(int, _jinja2_version.split('.', maxsplit=2)[:2]))
9
+ _CURRENT_JINJA_VERSION = tuple(map(int, jinja2_version.split('.', maxsplit=2)[:2]))
8
10
 
9
11
  if _CURRENT_JINJA_VERSION < _MINIMUM_JINJA_VERSION:
10
- raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {_jinja2_version}).')
12
+ raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {jinja2_version}).')
@@ -60,6 +60,7 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
60
60
  date=item.deprecated.date,
61
61
  obj=item.template,
62
62
  deprecator=item.deprecated.deprecator,
63
+ formatted_traceback=item.deprecated.formatted_traceback,
63
64
  )
64
65
 
65
66
  return result
@@ -79,7 +80,7 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
79
80
  # DTFIX-FUTURE: ascend the template stack to try and find the nearest string source template
80
81
  origin = Origin.get_tag(template)
81
82
 
82
- # DTFIX-RELEASE: this should probably use a synthesized description value on the tag
83
+ # DTFIX-FUTURE: this should probably use a synthesized description value on the tag
83
84
  # it is reachable from the data_tagging_controller test: ../playbook_output_validator/filter.py actual_stdout.txt actual_stderr.txt
84
85
  # -[DEPRECATION WARNING]: `something_old` is deprecated, don't use it! This feature will be removed in version 1.2.3.
85
86
  # +[DEPRECATION WARNING]: While processing '<<container>>': `something_old` is deprecated, don't use it! This feature will be removed in ...
@@ -75,7 +75,7 @@ class TemplateOptions:
75
75
  value_for_omit: object = Omit
76
76
  escape_backslashes: bool = True
77
77
  preserve_trailing_newlines: bool = True
78
- # DTFIX-RELEASE: these aren't really overrides anymore, rename the dataclass and this field
78
+ # DTFIX-FUTURE: these aren't really overrides anymore, rename the dataclass and this field
79
79
  # also mention in docstring this has no effect unless used to template a string
80
80
  overrides: TemplateOverrides = TemplateOverrides.DEFAULT
81
81
 
@@ -122,7 +122,6 @@ class TemplateEngine:
122
122
  return new_engine
123
123
 
124
124
  def extend(self, marker_behavior: MarkerBehavior | None = None) -> t.Self:
125
- # DTFIX-RELEASE: bikeshed name, supported features
126
125
  new_templar = type(self)(
127
126
  loader=self._loader,
128
127
  variables=self._variables,
@@ -187,7 +186,7 @@ class TemplateEngine:
187
186
  @property
188
187
  def available_variables(self) -> dict[str, t.Any] | ChainMap[str, t.Any]:
189
188
  """Available variables this instance will use when templating."""
190
- # DTFIX-RELEASE: ensure that we're always accessing this as a shallow container-level snapshot, and eliminate uses of anything
189
+ # DTFIX3: ensure that we're always accessing this as a shallow container-level snapshot, and eliminate uses of anything
191
190
  # that directly mutates this value. _new_context may resolve this for us?
192
191
  if self._variables is None:
193
192
  self._variables = self._variables_factory() if self._variables_factory else {}
@@ -235,7 +234,7 @@ class TemplateEngine:
235
234
 
236
235
  def template(
237
236
  self,
238
- variable: t.Any, # DTFIX-RELEASE: once we settle the new/old API boundaries, rename this (here and in other methods)
237
+ variable: t.Any, # DTFIX-FUTURE: once we settle the new/old API boundaries, rename this (here and in other methods)
239
238
  *,
240
239
  options: TemplateOptions = TemplateOptions.DEFAULT,
241
240
  mode: TemplateMode = TemplateMode.DEFAULT,
@@ -49,6 +49,7 @@ from ._jinja_common import (
49
49
  TruncationMarker,
50
50
  validate_arg_type,
51
51
  JinjaCallContext,
52
+ _SandboxMode,
52
53
  )
53
54
  from ._jinja_plugins import JinjaPluginIntercept, _query, _lookup, _now, _wrap_plugin_output, get_first_marker_arg, _DirectCall, _jinja_const_template_warning
54
55
  from ._lazy_containers import (
@@ -71,6 +72,11 @@ from ansible.vars.hostvars import HostVars, HostVarsVars
71
72
  from ...module_utils.datatag import native_type_name
72
73
 
73
74
  JINJA2_OVERRIDE = '#jinja2:'
75
+ """
76
+ String values prefixed with this sequence are interpreted as templates, even without template delimiters.
77
+ The values following this prefix up to the first newline are parsed as Jinja2 template overrides.
78
+ To include this literal value at the start of a string, a space or other character must precede it.
79
+ """
74
80
 
75
81
  display = Display()
76
82
 
@@ -304,7 +310,7 @@ class AnsibleTemplate(Template):
304
310
  _python_source_temp_path: pathlib.Path | None = None
305
311
 
306
312
  def __del__(self):
307
- # DTFIX-RELEASE: this still isn't working reliably; something else must be keeping the template object alive
313
+ # DTFIX-FUTURE: this still isn't working reliably; something else must be keeping the template object alive
308
314
  if self._python_source_temp_path:
309
315
  self._python_source_temp_path.unlink(missing_ok=True)
310
316
 
@@ -497,7 +503,7 @@ def create_template_error(ex: Exception, variable: t.Any, is_expression: bool) -
497
503
  return exception_to_raise
498
504
 
499
505
 
500
- # DTFIX-RELEASE: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
506
+ # DTFIX3: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
501
507
  # also update the protomatter integration test once this is done (the test was written differently since this wasn't done yet)
502
508
 
503
509
  _BUILTIN_FILTER_ALIASES: dict[str, str] = {}
@@ -583,10 +589,17 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
583
589
 
584
590
  return template_obj
585
591
 
592
+ def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
593
+ # deprecated: description="remove relaxed template sandbox mode support" core_version="2.23"
594
+ if _TemplateConfig.sandbox_mode == _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES:
595
+ return True
596
+
597
+ return super().is_safe_attribute(obj, attr, value)
598
+
586
599
  @property
587
600
  def lexer(self) -> AnsibleLexer:
588
601
  """Return/cache an AnsibleLexer with settings from the current AnsibleEnvironment"""
589
- # DTFIX-RELEASE: optimization - we should pre-generate the default cached lexer before forking, not leave it to chance (e.g. simple playbooks)
602
+ # DTFIX-FUTURE: optimization - we should pre-generate the default cached lexer before forking, not leave it to chance (e.g. simple playbooks)
590
603
  key = tuple(getattr(self, name) for name in _TEMPLATE_OVERRIDE_FIELD_NAMES)
591
604
 
592
605
  lex = self._lexer_cache.get(key)
@@ -610,7 +623,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
610
623
  Without this, `_wrap_filter` will wrap `args` and `kwargs` in templating lazy containers.
611
624
  This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
612
625
  """
613
- # DTFIX-RELEASE: need better logic to handle non-list/non-dict inputs for args/kwargs
626
+ # DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
614
627
  args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
615
628
  kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
616
629
 
@@ -630,7 +643,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
630
643
  Without this, `_wrap_test` will wrap `args` and `kwargs` in templating lazy containers.
631
644
  This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
632
645
  """
633
- # DTFIX-RELEASE: need better logic to handle non-list/non-dict inputs for args/kwargs
646
+ # DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
634
647
  args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
635
648
  kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
636
649
 
@@ -701,7 +714,6 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
701
714
  # this code is complemented by our tweaked CodeGenerator _output_const_repr that ensures that literal constants
702
715
  # in templates aren't double-repr'd in the generated code
703
716
  if len(node_list) == 1:
704
- # DTFIX-RELEASE: determine if we should do managed access here (we *should* have hit them all during templating/resolve, but ?)
705
717
  return node_list[0]
706
718
 
707
719
  # In order to ensure that all markers are tripped, do a recursive finalize before we repr (otherwise we can end up
@@ -856,9 +868,6 @@ def _flatten_and_lazify_vars(mapping: c.Mapping) -> t.Iterable[c.Mapping]:
856
868
  for m in mapping.maps:
857
869
  yield from _flatten_and_lazify_vars(m)
858
870
  elif mapping_type is _AnsibleLazyTemplateDict:
859
- if not mapping:
860
- # DTFIX-RELEASE: handle or remove?
861
- raise Exception("we didn't think it was possible to have an empty lazy here...")
862
871
  yield mapping
863
872
  elif mapping_type in (dict, _AnsibleTaggedDict):
864
873
  # don't propagate empty dictionary layers
@@ -882,10 +891,6 @@ def _new_context(
882
891
  layers = []
883
892
 
884
893
  if jinja_locals:
885
- # DTFIX-RELEASE: if we can't trip this in coverage, kill it off?
886
- if type(jinja_locals) is not dict: # pylint: disable=unidiomatic-typecheck
887
- raise NotImplementedError("locals must be a dict")
888
-
889
894
  # Omit values set to Jinja's internal `missing` sentinel; they are locals that have not yet been
890
895
  # initialized in the current context, and should not be exposed to child contexts. e.g.: {% import 'a' as b with context %}.
891
896
  # The `b` local will be `missing` in the `a` context and should not be propagated as a local to the child context we're creating.
@@ -978,7 +983,7 @@ def _finalize_list(o: t.Any, mode: FinalizeMode) -> t.Iterator[t.Any]:
978
983
 
979
984
 
980
985
  def _maybe_finalize_scalar(o: t.Any) -> t.Any:
981
- # DTFIX-RELEASE: this should check all supported scalar subclasses, not just JSON ones (also, does the JSON serializer handle these cases?)
986
+ # DTFIX5: this should check all supported scalar subclasses, not just JSON ones (also, does the JSON serializer handle these cases?)
982
987
  for target_type in _json_subclassable_scalar_types:
983
988
  if not isinstance(o, target_type):
984
989
  continue
@@ -1028,7 +1033,7 @@ def _finalize_collection(
1028
1033
 
1029
1034
  def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
1030
1035
  """Recurse the template result, rendering any encountered templates, converting containers to non-lazy versions."""
1031
- # DTFIX-RELEASE: add tests to ensure this method doesn't drift from allowed types
1036
+ # DTFIX5: add tests to ensure this method doesn't drift from allowed types
1032
1037
  o_type = type(o)
1033
1038
 
1034
1039
  # DTFIX-FUTURE: provide an optional way to check for trusted templates leaking out of templating (injected, but not passed through templar.template)
@@ -1045,7 +1050,7 @@ def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
1045
1050
  if o_type in _FINALIZE_FAST_PATH_EXACT_ITERABLE_TYPES: # silently convert known sequence types to list
1046
1051
  return _finalize_collection(o, mode, _finalize_list, list)
1047
1052
 
1048
- if o_type in Marker.concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
1053
+ if o_type in Marker._concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
1049
1054
  return TemplateContext.current().templar.marker_behavior.handle_marker(o)
1050
1055
 
1051
1056
  if mode is not FinalizeMode.TOP_LEVEL: # unsupported type (do not raise)