ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b6__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 (225) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_ansiballz/__init__.py +0 -0
  3. ansible/_internal/_ansiballz/_builder.py +101 -0
  4. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  5. ansible/_internal/_collection_proxy.py +1 -1
  6. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  7. ansible/_internal/_errors/_captured.py +25 -30
  8. ansible/_internal/_errors/_error_factory.py +89 -0
  9. ansible/_internal/_errors/_error_utils.py +240 -0
  10. ansible/_internal/_errors/_task_timeout.py +28 -0
  11. ansible/_internal/_event_formatting.py +127 -0
  12. ansible/_internal/_json/__init__.py +5 -5
  13. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  14. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  15. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  16. ansible/_internal/_ssh/__init__.py +0 -0
  17. ansible/_internal/_ssh/_agent_launch.py +91 -0
  18. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  19. ansible/_internal/_templating/__init__.py +5 -3
  20. ansible/_internal/_templating/_datatag.py +2 -1
  21. ansible/_internal/_templating/_engine.py +3 -4
  22. ansible/_internal/_templating/_jinja_bits.py +28 -20
  23. ansible/_internal/_templating/_jinja_common.py +18 -27
  24. ansible/_internal/_templating/_jinja_plugins.py +36 -5
  25. ansible/_internal/_templating/_lazy_containers.py +5 -5
  26. ansible/_internal/_templating/_template_vars.py +72 -0
  27. ansible/_internal/_templating/_transform.py +26 -19
  28. ansible/_internal/_templating/_utils.py +1 -1
  29. ansible/_internal/_yaml/_constructor.py +4 -4
  30. ansible/_internal/_yaml/_dumper.py +26 -18
  31. ansible/_internal/_yaml/_errors.py +7 -7
  32. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  33. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  34. ansible/cli/__init__.py +11 -93
  35. ansible/cli/arguments/option_helpers.py +3 -4
  36. ansible/cli/console.py +1 -1
  37. ansible/cli/doc.py +86 -30
  38. ansible/cli/inventory.py +5 -7
  39. ansible/compat/importlib_resources.py +9 -12
  40. ansible/config/base.yml +46 -0
  41. ansible/errors/__init__.py +98 -50
  42. ansible/executor/module_common.py +75 -49
  43. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  44. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  45. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  46. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  47. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  48. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  49. ansible/executor/powershell/module_manifest.py +52 -0
  50. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  51. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  52. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  53. ansible/executor/process/worker.py +40 -115
  54. ansible/executor/task_executor.py +26 -61
  55. ansible/executor/task_result.py +2 -4
  56. ansible/galaxy/api.py +1 -4
  57. ansible/galaxy/collection/__init__.py +2 -10
  58. ansible/galaxy/collection/concrete_artifact_manager.py +2 -8
  59. ansible/galaxy/role.py +2 -2
  60. ansible/inventory/manager.py +1 -1
  61. ansible/module_utils/_internal/__init__.py +7 -7
  62. ansible/module_utils/_internal/_ambient_context.py +3 -3
  63. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  64. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  65. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  66. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  67. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +13 -39
  68. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  69. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  70. ansible/module_utils/_internal/_datatag/__init__.py +43 -15
  71. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  72. ansible/module_utils/_internal/_deprecator.py +67 -55
  73. ansible/module_utils/_internal/_errors.py +88 -17
  74. ansible/module_utils/_internal/_event_utils.py +61 -0
  75. ansible/module_utils/_internal/_json/_profiles/__init__.py +22 -4
  76. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  77. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  78. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  79. ansible/module_utils/{common/messages.py → _internal/_messages.py} +54 -49
  80. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  81. ansible/module_utils/_internal/_plugin_info.py +15 -2
  82. ansible/module_utils/_internal/_stack.py +22 -0
  83. ansible/module_utils/_internal/_text_utils.py +6 -0
  84. ansible/module_utils/_internal/_traceback.py +11 -8
  85. ansible/module_utils/ansible_release.py +1 -1
  86. ansible/module_utils/basic.py +95 -71
  87. ansible/module_utils/common/arg_spec.py +2 -2
  88. ansible/module_utils/common/collections.py +6 -0
  89. ansible/module_utils/common/json.py +2 -2
  90. ansible/module_utils/common/respawn.py +4 -41
  91. ansible/module_utils/common/text/converters.py +3 -3
  92. ansible/module_utils/common/validation.py +1 -1
  93. ansible/module_utils/common/warnings.py +80 -23
  94. ansible/module_utils/common/yaml.py +1 -1
  95. ansible/module_utils/connection.py +8 -11
  96. ansible/module_utils/datatag.py +5 -2
  97. ansible/module_utils/facts/hardware/linux.py +1 -1
  98. ansible/module_utils/facts/sysctl.py +4 -6
  99. ansible/module_utils/facts/system/caps.py +2 -2
  100. ansible/module_utils/facts/system/distribution.py +16 -3
  101. ansible/module_utils/facts/system/local.py +1 -1
  102. ansible/module_utils/facts/virtual/linux.py +2 -2
  103. ansible/module_utils/service.py +3 -10
  104. ansible/module_utils/urls.py +4 -4
  105. ansible/modules/apt_repository.py +17 -39
  106. ansible/modules/assemble.py +2 -2
  107. ansible/modules/async_status.py +13 -11
  108. ansible/modules/async_wrapper.py +12 -22
  109. ansible/modules/command.py +3 -3
  110. ansible/modules/copy.py +4 -4
  111. ansible/modules/cron.py +1 -1
  112. ansible/modules/dnf5.py +14 -22
  113. ansible/modules/file.py +16 -17
  114. ansible/modules/find.py +3 -3
  115. ansible/modules/get_url.py +17 -0
  116. ansible/modules/git.py +9 -7
  117. ansible/modules/hostname.py +0 -1
  118. ansible/modules/known_hosts.py +12 -14
  119. ansible/modules/package.py +6 -0
  120. ansible/modules/replace.py +2 -2
  121. ansible/modules/service.py +3 -9
  122. ansible/modules/slurp.py +10 -13
  123. ansible/modules/stat.py +5 -7
  124. ansible/modules/unarchive.py +6 -6
  125. ansible/modules/user.py +1 -1
  126. ansible/modules/wait_for.py +28 -30
  127. ansible/modules/yum_repository.py +4 -3
  128. ansible/parsing/ajson.py +3 -5
  129. ansible/parsing/dataloader.py +6 -6
  130. ansible/parsing/mod_args.py +1 -1
  131. ansible/parsing/plugin_docs.py +2 -2
  132. ansible/parsing/utils/yaml.py +3 -3
  133. ansible/parsing/vault/__init__.py +10 -14
  134. ansible/playbook/base.py +7 -2
  135. ansible/playbook/included_file.py +3 -1
  136. ansible/playbook/play_context.py +2 -0
  137. ansible/playbook/playbook_include.py +1 -1
  138. ansible/playbook/taggable.py +19 -8
  139. ansible/playbook/task.py +2 -0
  140. ansible/plugins/__init__.py +0 -25
  141. ansible/plugins/action/__init__.py +8 -31
  142. ansible/plugins/action/add_host.py +1 -1
  143. ansible/plugins/action/assemble.py +8 -16
  144. ansible/plugins/action/async_status.py +7 -2
  145. ansible/plugins/action/copy.py +8 -7
  146. ansible/plugins/action/fetch.py +3 -3
  147. ansible/plugins/action/gather_facts.py +8 -8
  148. ansible/plugins/action/package.py +5 -8
  149. ansible/plugins/action/script.py +8 -15
  150. ansible/plugins/action/service.py +3 -7
  151. ansible/plugins/action/template.py +11 -10
  152. ansible/plugins/action/unarchive.py +5 -15
  153. ansible/plugins/action/uri.py +9 -20
  154. ansible/plugins/cache/__init__.py +17 -19
  155. ansible/plugins/callback/__init__.py +4 -6
  156. ansible/plugins/callback/junit.py +4 -2
  157. ansible/plugins/callback/tree.py +5 -5
  158. ansible/plugins/connection/local.py +6 -6
  159. ansible/plugins/connection/paramiko_ssh.py +5 -5
  160. ansible/plugins/connection/ssh.py +25 -15
  161. ansible/plugins/connection/winrm.py +6 -3
  162. ansible/plugins/doc_fragments/constructed.py +2 -2
  163. ansible/plugins/filter/core.py +32 -27
  164. ansible/plugins/filter/encryption.py +14 -6
  165. ansible/plugins/inventory/__init__.py +11 -10
  166. ansible/plugins/inventory/script.py +1 -1
  167. ansible/plugins/list.py +73 -19
  168. ansible/plugins/loader.py +7 -7
  169. ansible/plugins/lookup/csvfile.py +16 -71
  170. ansible/plugins/lookup/first_found.py +2 -1
  171. ansible/plugins/lookup/template.py +9 -4
  172. ansible/plugins/shell/__init__.py +56 -2
  173. ansible/plugins/shell/powershell.py +67 -9
  174. ansible/plugins/shell/sh.py +10 -5
  175. ansible/plugins/strategy/__init__.py +3 -3
  176. ansible/plugins/test/core.py +22 -16
  177. ansible/plugins/test/finished.yml +1 -1
  178. ansible/plugins/test/uri.py +2 -5
  179. ansible/release.py +1 -1
  180. ansible/template/__init__.py +38 -54
  181. ansible/utils/collection_loader/_collection_finder.py +3 -3
  182. ansible/utils/display.py +124 -138
  183. ansible/utils/galaxy.py +2 -2
  184. ansible/utils/hashing.py +6 -8
  185. ansible/utils/listify.py +6 -4
  186. ansible/utils/path.py +5 -7
  187. ansible/utils/py3compat.py +2 -1
  188. ansible/utils/ssh_functions.py +3 -2
  189. ansible/utils/unsafe_proxy.py +1 -1
  190. ansible/vars/hostvars.py +1 -1
  191. ansible/vars/plugins.py +3 -3
  192. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/METADATA +1 -1
  193. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/RECORD +224 -204
  194. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/WHEEL +1 -1
  195. ansible_test/_data/completion/docker.txt +3 -3
  196. ansible_test/_data/completion/remote.txt +1 -0
  197. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  198. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  199. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  200. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  201. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  202. ansible_test/_internal/commands/integration/coverage.py +7 -2
  203. ansible_test/_internal/host_profiles.py +62 -10
  204. ansible_test/_internal/provisioning.py +10 -4
  205. ansible_test/_internal/ssh.py +1 -5
  206. ansible_test/_internal/thread.py +2 -1
  207. ansible_test/_internal/timeout.py +1 -1
  208. ansible_test/_internal/util.py +40 -12
  209. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  210. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  211. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  212. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  213. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  214. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  215. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  216. ansible_test/_util/target/setup/requirements.py +3 -9
  217. ansible/_internal/_errors/_utils.py +0 -310
  218. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/entry_points.txt +0 -0
  219. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/COPYING +0 -0
  220. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  221. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  222. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  223. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  224. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  225. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
- base image=quay.io/ansible/base-test-container:8.1.0 python=3.13,3.8,3.9,3.10,3.11,3.12
2
- default image=quay.io/ansible/default-test-container:11.5.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection
3
- default image=quay.io/ansible/ansible-core-test-container:11.5.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
1
+ base image=quay.io/ansible/base-test-container:8.2.0 python=3.13,3.8,3.9,3.10,3.11,3.12
2
+ default image=quay.io/ansible/default-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection
3
+ default image=quay.io/ansible/ansible-core-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
4
4
  alpine321 image=quay.io/ansible/alpine321-test-container:9.1.0 python=3.12 cgroup=none audit=none
5
5
  fedora41 image=quay.io/ansible/fedora41-test-container:9.0.0 python=3.13 cgroup=v2-only
6
6
  ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:9.0.0 python=3.10
@@ -8,6 +8,7 @@ freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
8
8
  macos/15.3 python=3.13 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
9
9
  macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
10
10
  rhel/9.5 python=3.9,3.12 become=sudo provider=aws arch=x86_64
11
+ rhel/10.0 python=3.12 become=sudo provider=aws arch=x86_64
11
12
  rhel become=sudo provider=aws arch=x86_64
12
13
  ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64
13
14
  ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64
@@ -1,5 +1,5 @@
1
1
  # edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc
2
2
  Jinja2==3.1.6
3
3
  MarkupSafe==3.0.2
4
- packaging==24.2
4
+ packaging==25.0
5
5
  PyYAML==6.0.2
@@ -1,9 +1,9 @@
1
1
  # edit "sanity.changelog.in" and generate with: hacking/update-sanity-requirements.py --test changelog
2
2
  antsibull-changelog==0.29.0
3
3
  docutils==0.18.1
4
- packaging==24.2
4
+ packaging==25.0
5
5
  PyYAML==6.0.2
6
6
  rstcheck==5.0.0
7
7
  semantic-version==2.10.0
8
8
  types-docutils==0.18.3
9
- typing_extensions==4.12.2
9
+ typing_extensions==4.13.2
@@ -1,2 +1,2 @@
1
1
  # edit "sanity.pep8.in" and generate with: hacking/update-sanity-requirements.py --test pep8
2
- pycodestyle==2.12.1
2
+ pycodestyle==2.13.0
@@ -1,9 +1,9 @@
1
1
  # edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint
2
- astroid==3.3.9
3
- dill==0.3.9
2
+ astroid==3.3.10
3
+ dill==0.4.0
4
4
  isort==6.0.1
5
5
  mccabe==0.7.0
6
- platformdirs==4.3.7
7
- pylint==3.3.6
6
+ platformdirs==4.3.8
7
+ pylint==3.3.7
8
8
  PyYAML==6.0.2
9
9
  tomlkit==0.13.2
@@ -1,4 +1,4 @@
1
1
  # edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint
2
2
  pathspec==0.12.1
3
3
  PyYAML==6.0.2
4
- yamllint==1.36.2
4
+ yamllint==1.37.1
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
+ import json
6
7
  import os
7
8
  import shutil
8
9
  import tempfile
@@ -240,9 +241,13 @@ class PosixCoverageHandler(CoverageHandler[PosixConfig]):
240
241
  # cause the 'coverage' module to be found, but not imported or enabled
241
242
  coverage_file = ''
242
243
 
244
+ coverage_options = dict(
245
+ config=config_file,
246
+ output=coverage_file,
247
+ )
248
+
243
249
  variables = dict(
244
- _ANSIBLE_COVERAGE_CONFIG=config_file,
245
- _ANSIBLE_COVERAGE_OUTPUT=coverage_file,
250
+ _ANSIBLE_ANSIBALLZ_COVERAGE_CONFIG=json.dumps(coverage_options),
246
251
  )
247
252
 
248
253
  return variables
@@ -206,7 +206,7 @@ class Inventory:
206
206
  inventory_text += f'[{group}]\n'
207
207
 
208
208
  for host, variables in hosts.items():
209
- kvp = ' '.join(f'{key}="{value}"' for key, value in variables.items())
209
+ kvp = ' '.join(f"{key}={value!r}" for key, value in variables.items())
210
210
  inventory_text += f'{host} {kvp}\n'
211
211
 
212
212
  inventory_text += '\n'
@@ -235,18 +235,24 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
235
235
  *,
236
236
  args: EnvironmentConfig,
237
237
  config: THostConfig,
238
- targets: t.Optional[list[HostConfig]],
238
+ controller: ControllerHostProfile,
239
239
  ) -> None:
240
240
  self.args = args
241
241
  self.config = config
242
- self.controller = bool(targets)
243
- self.targets = targets or []
242
+ self.controller = not controller # this profile is a controller whenever the `controller` arg was not provided
243
+ self.targets = args.targets if self.controller else [] # only keep targets if this profile is a controller
244
+ self.controller_profile = controller if isinstance(self, ControllerProfile) else None
244
245
 
245
246
  self.state: dict[str, t.Any] = {}
246
247
  """State that must be persisted across delegation."""
247
248
  self.cache: dict[str, t.Any] = {}
248
249
  """Cache that must not be persisted across delegation."""
249
250
 
251
+ @property
252
+ @abc.abstractmethod
253
+ def name(self) -> str:
254
+ """The name of the host profile."""
255
+
250
256
  def provision(self) -> None:
251
257
  """Provision the host before delegation."""
252
258
 
@@ -274,6 +280,9 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
274
280
  # args will be populated after the instances are restored
275
281
  self.cache = {}
276
282
 
283
+ def __str__(self) -> str:
284
+ return f'{self.__class__.__name__}: {self.name}'
285
+
277
286
 
278
287
  class PosixProfile(HostProfile[TPosixConfig], metaclass=abc.ABCMeta):
279
288
  """Base class for POSIX host profiles."""
@@ -320,6 +329,11 @@ class SshTargetHostProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
320
329
  class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
321
330
  """Base class for remote instance profiles."""
322
331
 
332
+ @property
333
+ def name(self) -> str:
334
+ """The name of the host profile."""
335
+ return self.config.name
336
+
323
337
  @property
324
338
  def core_ci_state(self) -> t.Optional[dict[str, str]]:
325
339
  """The saved Ansible Core CI state."""
@@ -339,6 +353,8 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
339
353
 
340
354
  def deprovision(self) -> None:
341
355
  """Deprovision the host after delegation has completed."""
356
+ super().deprovision()
357
+
342
358
  if self.args.remote_terminate == TerminateMode.ALWAYS or (self.args.remote_terminate == TerminateMode.SUCCESS and self.args.success):
343
359
  self.delete_instance()
344
360
 
@@ -397,6 +413,11 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
397
413
  class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[ControllerConfig]):
398
414
  """Host profile for the controller as a target."""
399
415
 
416
+ @property
417
+ def name(self) -> str:
418
+ """The name of the host profile."""
419
+ return self.controller_profile.name
420
+
400
421
  def get_controller_target_connections(self) -> list[SshConnection]:
401
422
  """Return SSH connection(s) for accessing the host as a target from the controller."""
402
423
  settings = SshConnectionDetail(
@@ -425,6 +446,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
425
446
  command_privileged: bool
426
447
  expected_mounts: tuple[CGroupMount, ...]
427
448
 
449
+ @property
450
+ def name(self) -> str:
451
+ """The name of the host profile."""
452
+ return self.config.name
453
+
428
454
  @property
429
455
  def container_name(self) -> t.Optional[str]:
430
456
  """Return the stored container name, if any, otherwise None."""
@@ -976,6 +1002,8 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
976
1002
 
977
1003
  def deprovision(self) -> None:
978
1004
  """Deprovision the host after delegation has completed."""
1005
+ super().deprovision()
1006
+
979
1007
  container_exists = False
980
1008
 
981
1009
  if self.container_name:
@@ -1025,10 +1053,10 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
1025
1053
 
1026
1054
  raise HostConnectionError(f'Timeout waiting for {self.config.name} container {self.container_name}.', callback)
1027
1055
 
1028
- def get_controller_target_connections(self) -> list[SshConnection]:
1029
- """Return SSH connection(s) for accessing the host as a target from the controller."""
1056
+ def get_ssh_connection_detail(self, host_type: str) -> SshConnectionDetail:
1057
+ """Return SSH connection detail for the specified host type."""
1030
1058
  containers = get_container_database(self.args)
1031
- access = containers.data[HostType.control]['__test_hosts__'][self.container_name]
1059
+ access = containers.data[host_type]['__test_hosts__'][self.container_name]
1032
1060
 
1033
1061
  host = access.host_ip
1034
1062
  port = dict(access.port_map())[22]
@@ -1046,7 +1074,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
1046
1074
  enable_rsa_sha1='centos6' in self.config.image,
1047
1075
  )
1048
1076
 
1049
- return [SshConnection(self.args, settings)]
1077
+ return settings
1078
+
1079
+ def get_controller_target_connections(self) -> list[SshConnection]:
1080
+ """Return SSH connection(s) for accessing the host as a target from the controller."""
1081
+ return [SshConnection(self.args, self.get_ssh_connection_detail(HostType.control))]
1050
1082
 
1051
1083
  def get_origin_controller_connection(self) -> DockerConnection:
1052
1084
  """Return a connection for accessing the host as a controller from the origin."""
@@ -1116,6 +1148,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
1116
1148
  class NetworkInventoryProfile(HostProfile[NetworkInventoryConfig]):
1117
1149
  """Host profile for a network inventory."""
1118
1150
 
1151
+ @property
1152
+ def name(self) -> str:
1153
+ """The name of the host profile."""
1154
+ return self.config.path
1155
+
1119
1156
 
1120
1157
  class NetworkRemoteProfile(RemoteProfile[NetworkRemoteConfig]):
1121
1158
  """Host profile for a network remote instance."""
@@ -1197,6 +1234,11 @@ class NetworkRemoteProfile(RemoteProfile[NetworkRemoteConfig]):
1197
1234
  class OriginProfile(ControllerHostProfile[OriginConfig]):
1198
1235
  """Host profile for origin."""
1199
1236
 
1237
+ @property
1238
+ def name(self) -> str:
1239
+ """The name of the host profile."""
1240
+ return 'origin'
1241
+
1200
1242
  def get_origin_controller_connection(self) -> LocalConnection:
1201
1243
  """Return a connection for accessing the host as a controller from the origin."""
1202
1244
  return LocalConnection(self.args)
@@ -1317,6 +1359,11 @@ class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile
1317
1359
  class PosixSshProfile(SshTargetHostProfile[PosixSshConfig], PosixProfile[PosixSshConfig]):
1318
1360
  """Host profile for a POSIX SSH instance."""
1319
1361
 
1362
+ @property
1363
+ def name(self) -> str:
1364
+ """The name of the host profile."""
1365
+ return self.config.host
1366
+
1320
1367
  def get_controller_target_connections(self) -> list[SshConnection]:
1321
1368
  """Return SSH connection(s) for accessing the host as a target from the controller."""
1322
1369
  settings = SshConnectionDetail(
@@ -1334,6 +1381,11 @@ class PosixSshProfile(SshTargetHostProfile[PosixSshConfig], PosixProfile[PosixSs
1334
1381
  class WindowsInventoryProfile(SshTargetHostProfile[WindowsInventoryConfig]):
1335
1382
  """Host profile for a Windows inventory."""
1336
1383
 
1384
+ @property
1385
+ def name(self) -> str:
1386
+ """The name of the host profile."""
1387
+ return self.config.path
1388
+
1337
1389
  def get_controller_target_connections(self) -> list[SshConnection]:
1338
1390
  """Return SSH connection(s) for accessing the host as a target from the controller."""
1339
1391
  inventory = parse_inventory(self.args, self.config.path)
@@ -1436,9 +1488,9 @@ def get_config_profile_type_map() -> dict[t.Type[HostConfig], t.Type[HostProfile
1436
1488
  def create_host_profile(
1437
1489
  args: EnvironmentConfig,
1438
1490
  config: HostConfig,
1439
- controller: bool,
1491
+ controller: ControllerHostProfile | None,
1440
1492
  ) -> HostProfile:
1441
1493
  """Create and return a host profile from the given host configuration."""
1442
1494
  profile_type = get_config_profile_type_map()[type(config)]
1443
- profile = profile_type(args=args, config=config, targets=args.targets if controller else None)
1495
+ profile = profile_type(args=args, config=config, controller=controller)
1444
1496
  return profile
@@ -116,9 +116,11 @@ def prepare_profiles(
116
116
  else:
117
117
  run_pypi_proxy(args, targets_use_pypi)
118
118
 
119
+ controller_host_profile = t.cast(ControllerHostProfile, create_host_profile(args, args.controller, None))
120
+
119
121
  host_state = HostState(
120
- controller_profile=t.cast(ControllerHostProfile, create_host_profile(args, args.controller, True)),
121
- target_profiles=[create_host_profile(args, target, False) for target in args.targets],
122
+ controller_profile=controller_host_profile,
123
+ target_profiles=[create_host_profile(args, target, controller_host_profile) for target in args.targets],
122
124
  )
123
125
 
124
126
  if args.prime_containers:
@@ -137,7 +139,9 @@ def prepare_profiles(
137
139
  if not skip_setup:
138
140
  profile.setup()
139
141
 
140
- dispatch_jobs([(profile, WrappedThread(functools.partial(provision, profile))) for profile in host_state.profiles])
142
+ dispatch_jobs(
143
+ [(profile, WrappedThread(functools.partial(provision, profile), f'Provision: {profile}')) for profile in host_state.profiles]
144
+ )
141
145
 
142
146
  host_state.controller_profile.configure()
143
147
 
@@ -157,7 +161,9 @@ def prepare_profiles(
157
161
  if requirements:
158
162
  requirements(profile)
159
163
 
160
- dispatch_jobs([(profile, WrappedThread(functools.partial(configure, profile))) for profile in host_state.target_profiles])
164
+ dispatch_jobs(
165
+ [(profile, WrappedThread(functools.partial(configure, profile), f'Configure: {profile}')) for profile in host_state.target_profiles]
166
+ )
161
167
 
162
168
  return host_state
163
169
 
@@ -13,7 +13,6 @@ import shlex
13
13
  import typing as t
14
14
 
15
15
  from .encoding import (
16
- to_bytes,
17
16
  to_text,
18
17
  )
19
18
 
@@ -223,13 +222,10 @@ def run_ssh_command(
223
222
  cmd_show = shlex.join(cmd)
224
223
  display.info('Run background command: %s' % cmd_show, verbosity=1, truncate=True)
225
224
 
226
- cmd_bytes = [to_bytes(arg) for arg in cmd]
227
- env_bytes = dict((to_bytes(k), to_bytes(v)) for k, v in env.items())
228
-
229
225
  if args.explain:
230
226
  process = SshProcess(None)
231
227
  else:
232
- process = SshProcess(subprocess.Popen(cmd_bytes, env=env_bytes, bufsize=-1, # pylint: disable=consider-using-with
228
+ process = SshProcess(subprocess.Popen(cmd, env=env, bufsize=-1, # pylint: disable=consider-using-with
233
229
  stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE))
234
230
 
235
231
  return process
@@ -17,11 +17,12 @@ TCallable = t.TypeVar('TCallable', bound=t.Callable[..., t.Any])
17
17
  class WrappedThread(threading.Thread):
18
18
  """Wrapper around Thread which captures results and exceptions."""
19
19
 
20
- def __init__(self, action: c.Callable[[], t.Any]) -> None:
20
+ def __init__(self, action: c.Callable[[], t.Any], name: str) -> None:
21
21
  super().__init__()
22
22
  self._result: queue.Queue[t.Any] = queue.Queue()
23
23
  self.action = action
24
24
  self.result = None
25
+ self.name = name
25
26
 
26
27
  def run(self) -> None:
27
28
  """
@@ -126,6 +126,6 @@ def configure_test_timeout(args: TestConfig) -> None:
126
126
 
127
127
  signal.signal(signal.SIGUSR1, timeout_handler)
128
128
 
129
- instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.total_seconds()))
129
+ instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.total_seconds()), 'Timeout Watchdog')
130
130
  instance.daemon = True
131
131
  instance.start()
@@ -254,12 +254,29 @@ def filter_args(args: list[str], filters: dict[str, int]) -> list[str]:
254
254
  """Return a filtered version of the given command line arguments."""
255
255
  remaining = 0
256
256
  result = []
257
+ pass_through_args: list[str] = []
258
+ pass_through_explicit = False
259
+ pass_through_implicit = False
257
260
 
258
261
  for arg in args:
262
+ if pass_through_explicit:
263
+ pass_through_args.append(arg)
264
+ continue
265
+
266
+ if arg == '--':
267
+ pass_through_explicit = True
268
+ continue
269
+
259
270
  if not arg.startswith('-') and remaining:
260
271
  remaining -= 1
272
+ pass_through_implicit = not remaining
261
273
  continue
262
274
 
275
+ if not arg.startswith('-') and pass_through_implicit:
276
+ pass_through_args.append(arg)
277
+ continue
278
+
279
+ pass_through_implicit = False
263
280
  remaining = 0
264
281
 
265
282
  parts = arg.split('=', 1)
@@ -271,6 +288,9 @@ def filter_args(args: list[str], filters: dict[str, int]) -> list[str]:
271
288
 
272
289
  result.append(arg)
273
290
 
291
+ if pass_through_args:
292
+ result += ['--'] + pass_through_args
293
+
274
294
  return result
275
295
 
276
296
 
@@ -513,16 +533,23 @@ def raw_command(
513
533
 
514
534
  try:
515
535
  try:
516
- cmd_bytes = [to_bytes(arg) for arg in cmd]
517
- env_bytes = dict((to_bytes(k), to_bytes(v)) for k, v in env.items())
518
- process = subprocess.Popen(cmd_bytes, env=env_bytes, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd) # pylint: disable=consider-using-with
536
+ process = subprocess.Popen(cmd, env=env, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd) # pylint: disable=consider-using-with
519
537
  except FileNotFoundError as ex:
520
538
  raise ApplicationError('Required program "%s" not found.' % cmd[0]) from ex
521
539
 
522
540
  if communicate:
523
541
  data_bytes = to_optional_bytes(data)
524
- stdout_bytes, stderr_bytes = communicate_with_process(process, data_bytes, stdout == subprocess.PIPE, stderr == subprocess.PIPE, capture=capture,
525
- output_stream=output_stream)
542
+
543
+ stdout_bytes, stderr_bytes = communicate_with_process(
544
+ name=cmd[0],
545
+ process=process,
546
+ stdin=data_bytes,
547
+ stdout=stdout == subprocess.PIPE,
548
+ stderr=stderr == subprocess.PIPE,
549
+ capture=capture,
550
+ output_stream=output_stream,
551
+ )
552
+
526
553
  stdout_text = to_optional_text(stdout_bytes, str_errors) or ''
527
554
  stderr_text = to_optional_text(stderr_bytes, str_errors) or ''
528
555
  else:
@@ -546,6 +573,7 @@ def raw_command(
546
573
 
547
574
 
548
575
  def communicate_with_process(
576
+ name: str,
549
577
  process: subprocess.Popen,
550
578
  stdin: t.Optional[bytes],
551
579
  stdout: bool,
@@ -563,16 +591,16 @@ def communicate_with_process(
563
591
  reader = OutputThread
564
592
 
565
593
  if stdin is not None:
566
- threads.append(WriterThread(process.stdin, stdin))
594
+ threads.append(WriterThread(process.stdin, stdin, name))
567
595
 
568
596
  if stdout:
569
- stdout_reader = reader(process.stdout, output_stream.get_buffer(sys.stdout.buffer))
597
+ stdout_reader = reader(process.stdout, output_stream.get_buffer(sys.stdout.buffer), name)
570
598
  threads.append(stdout_reader)
571
599
  else:
572
600
  stdout_reader = None
573
601
 
574
602
  if stderr:
575
- stderr_reader = reader(process.stderr, output_stream.get_buffer(sys.stderr.buffer))
603
+ stderr_reader = reader(process.stderr, output_stream.get_buffer(sys.stderr.buffer), name)
576
604
  threads.append(stderr_reader)
577
605
  else:
578
606
  stderr_reader = None
@@ -604,8 +632,8 @@ def communicate_with_process(
604
632
  class WriterThread(WrappedThread):
605
633
  """Thread to write data to stdin of a subprocess."""
606
634
 
607
- def __init__(self, handle: t.IO[bytes], data: bytes) -> None:
608
- super().__init__(self._run)
635
+ def __init__(self, handle: t.IO[bytes], data: bytes, name: str) -> None:
636
+ super().__init__(self._run, f'{self.__class__.__name__}: {name}')
609
637
 
610
638
  self.handle = handle
611
639
  self.data = data
@@ -622,8 +650,8 @@ class WriterThread(WrappedThread):
622
650
  class ReaderThread(WrappedThread, metaclass=abc.ABCMeta):
623
651
  """Thread to read stdout from a subprocess."""
624
652
 
625
- def __init__(self, handle: t.IO[bytes], buffer: t.BinaryIO) -> None:
626
- super().__init__(self._run)
653
+ def __init__(self, handle: t.IO[bytes], buffer: t.BinaryIO, name: str) -> None:
654
+ super().__init__(self._run, f'{self.__class__.__name__}: {name}')
627
655
 
628
656
  self.handle = handle
629
657
  self.buffer = buffer
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # Python 2.x support still required
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # many occurrences
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # many occurrences
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -11,6 +11,7 @@ disable=
11
11
  attribute-defined-outside-init,
12
12
  bad-indentation,
13
13
  bad-mcs-classmethod-argument,
14
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
14
15
  broad-exception-caught,
15
16
  broad-exception-raised,
16
17
  c-extension-no-member,
@@ -16,6 +16,7 @@ disable=
16
16
  attribute-defined-outside-init,
17
17
  bad-indentation,
18
18
  bad-mcs-classmethod-argument,
19
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
19
20
  broad-exception-caught,
20
21
  broad-exception-raised,
21
22
  c-extension-no-member,
@@ -176,7 +176,6 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
176
176
  def __init__(self, *args, **kwargs) -> None:
177
177
  super().__init__(*args, **kwargs)
178
178
 
179
- self.inference_context = astroid.context.InferenceContext()
180
179
  self.module_cache: dict[str, astroid.Module] = {}
181
180
 
182
181
  @functools.cached_property
@@ -241,7 +240,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
241
240
  inferred: astroid.typing.InferenceResult | None = None
242
241
 
243
242
  while target:
244
- if inferred := astroid.util.safe_infer(target, self.inference_context):
243
+ if inferred := astroid.util.safe_infer(target):
245
244
  break
246
245
 
247
246
  if isinstance(target, astroid.Call):
@@ -263,6 +262,20 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
263
262
  break
264
263
 
265
264
  for name in reversed(names):
265
+ if isinstance(inferred, astroid.Instance):
266
+ try:
267
+ attr = next(iter(inferred.getattr(name)), None)
268
+ except astroid.AttributeInferenceError:
269
+ break
270
+
271
+ if isinstance(attr, astroid.AssignAttr):
272
+ inferred = self.get_ansible_module(attr)
273
+ continue
274
+
275
+ if isinstance(attr, astroid.FunctionDef):
276
+ inferred = attr
277
+ continue
278
+
266
279
  if not isinstance(inferred, (astroid.Module, astroid.ClassDef)):
267
280
  inferred = None
268
281
  break
@@ -282,25 +295,46 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
282
295
  def infer_name(self, node: astroid.Name) -> astroid.NodeNG | None:
283
296
  """Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred."""
284
297
  scope = node.scope()
285
- name = None
298
+ inferred: astroid.NodeNG | None = None
299
+ name = node.name
286
300
 
287
301
  while scope:
288
302
  try:
289
- assignment = scope[node.name]
303
+ assignment = scope[name]
290
304
  except KeyError:
291
305
  scope = scope.parent.scope() if scope.parent else None
292
306
  continue
293
307
 
294
308
  if isinstance(assignment, astroid.AssignName) and isinstance(assignment.parent, astroid.Assign):
295
- name = assignment.parent.value
309
+ inferred = assignment.parent.value
310
+ elif (
311
+ isinstance(scope, astroid.FunctionDef)
312
+ and isinstance(assignment, astroid.AssignName)
313
+ and isinstance(assignment.parent, astroid.Arguments)
314
+ and assignment.parent.annotations
315
+ ):
316
+ idx, _node = assignment.parent.find_argname(name)
317
+
318
+ if idx is not None:
319
+ try:
320
+ annotation = assignment.parent.annotations[idx]
321
+ except IndexError:
322
+ pass
323
+ else:
324
+ if isinstance(annotation, astroid.Name):
325
+ name = annotation.name
326
+ continue
327
+ elif isinstance(assignment, astroid.ClassDef):
328
+ inferred = assignment
296
329
  elif isinstance(assignment, astroid.ImportFrom):
297
330
  if module := self.get_module(assignment):
331
+ name = assignment.real_name(name)
298
332
  scope = module.scope()
299
333
  continue
300
334
 
301
335
  break
302
336
 
303
- return name
337
+ return inferred
304
338
 
305
339
  def get_module(self, node: astroid.ImportFrom) -> astroid.Module | None:
306
340
  """Import the requested module if possible and cache the result."""
@@ -480,7 +514,27 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
480
514
 
481
515
  raise TypeError(type(value))
482
516
 
517
+ def get_ansible_module(self, node: astroid.AssignAttr) -> astroid.Instance | None:
518
+ """Infer an AnsibleModule instance node from the given assignment."""
519
+ if isinstance(node.parent, astroid.Assign) and isinstance(node.parent.type_annotation, astroid.Name):
520
+ inferred = self.infer_name(node.parent.type_annotation)
521
+ elif isinstance(node.parent, astroid.Assign) and isinstance(node.parent.parent, astroid.FunctionDef) and isinstance(node.parent.value, astroid.Name):
522
+ inferred = self.infer_name(node.parent.value)
523
+ elif isinstance(node.parent, astroid.AnnAssign) and isinstance(node.parent.annotation, astroid.Name):
524
+ inferred = self.infer_name(node.parent.annotation)
525
+ else:
526
+ inferred = None
527
+
528
+ if isinstance(inferred, astroid.ClassDef) and inferred.name == 'AnsibleModule':
529
+ return inferred.instantiate_class()
530
+
531
+ return None
532
+
533
+ def register(self) -> None:
534
+ """Register this plugin."""
535
+ self.linter.register_checker(self)
536
+
483
537
 
484
538
  def register(linter: pylint.lint.PyLinter) -> None:
485
539
  """Required method to auto-register this checker."""
486
- linter.register_checker(AnsibleDeprecatedChecker(linter))
540
+ AnsibleDeprecatedChecker(linter).register()