ansible-core 2.19.0rc1__py3-none-any.whl → 2.19.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (48) hide show
  1. ansible/_internal/_ansiballz/_builder.py +25 -14
  2. ansible/_internal/_templating/_engine.py +6 -4
  3. ansible/_internal/_templating/_jinja_bits.py +3 -1
  4. ansible/_internal/_templating/_jinja_plugins.py +7 -2
  5. ansible/_internal/_templating/_lazy_containers.py +5 -5
  6. ansible/config/base.yml +16 -6
  7. ansible/config/manager.py +37 -16
  8. ansible/executor/task_executor.py +5 -2
  9. ansible/executor/task_queue_manager.py +2 -2
  10. ansible/module_utils/_internal/_ansiballz/_extensions/_debugpy.py +97 -0
  11. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +2 -4
  12. ansible/module_utils/_internal/_traceback.py +1 -1
  13. ansible/module_utils/ansible_release.py +1 -1
  14. ansible/module_utils/basic.py +10 -2
  15. ansible/module_utils/common/validation.py +4 -1
  16. ansible/modules/dnf.py +36 -50
  17. ansible/modules/dnf5.py +36 -29
  18. ansible/modules/meta.py +2 -1
  19. ansible/modules/service_facts.py +5 -1
  20. ansible/playbook/helpers.py +1 -0
  21. ansible/playbook/taggable.py +1 -2
  22. ansible/plugins/__init__.py +18 -10
  23. ansible/plugins/callback/__init__.py +6 -1
  24. ansible/plugins/filter/to_json.yml +8 -4
  25. ansible/plugins/filter/to_nice_json.yml +3 -2
  26. ansible/plugins/lookup/template.py +6 -1
  27. ansible/release.py +1 -1
  28. ansible/utils/encrypt.py +2 -0
  29. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/METADATA +1 -1
  30. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/RECORD +48 -47
  31. ansible_test/_internal/commands/integration/coverage.py +2 -2
  32. ansible_test/_internal/commands/shell/__init__.py +67 -28
  33. ansible_test/_internal/coverage_util.py +28 -25
  34. ansible_test/_internal/debugging.py +337 -49
  35. ansible_test/_internal/host_profiles.py +43 -43
  36. ansible_test/_internal/metadata.py +7 -42
  37. ansible_test/_internal/python_requirements.py +2 -2
  38. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  39. ansible_test/_util/target/setup/bootstrap.sh +37 -16
  40. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/WHEEL +0 -0
  41. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/entry_points.txt +0 -0
  42. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/COPYING +0 -0
  43. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  44. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  45. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  46. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  47. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  48. {ansible_core-2.19.0rc1.dist-info → ansible_core-2.19.1.dist-info}/top_level.txt +0 -0
@@ -2,16 +2,25 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import abc
5
6
  import dataclasses
7
+ import importlib
6
8
  import json
7
9
  import os
8
10
  import re
11
+ import sys
12
+ import typing as t
9
13
 
10
14
  from .util import (
11
15
  cache,
12
16
  display,
13
17
  raw_command,
14
18
  ApplicationError,
19
+ get_subclasses,
20
+ )
21
+
22
+ from .util_common import (
23
+ CommonConfig,
15
24
  )
16
25
 
17
26
  from .processes import (
@@ -24,76 +33,308 @@ from .config import (
24
33
  )
25
34
 
26
35
  from .metadata import (
27
- DebuggerSettings,
28
36
  DebuggerFlags,
29
37
  )
30
38
 
31
- from . import (
39
+ from .data import (
32
40
  data_context,
33
- CommonConfig,
34
41
  )
35
42
 
36
43
 
37
- def initialize_debugger(args: CommonConfig) -> None:
38
- """Initialize the debugger settings before delegation."""
39
- if not isinstance(args, EnvironmentConfig):
40
- return
44
+ class DebuggerProfile(t.Protocol):
45
+ """Protocol for debugger profiles."""
41
46
 
42
- if args.metadata.loaded:
43
- return # after delegation
47
+ @property
48
+ def debugger_host(self) -> str:
49
+ """The hostname to expose to the debugger."""
44
50
 
45
- if collection := data_context().content.collection:
46
- args.metadata.collection_root = collection.root
47
-
48
- load_debugger_settings(args)
51
+ @property
52
+ def debugger_port(self) -> int:
53
+ """The port to expose to the debugger."""
49
54
 
55
+ def get_source_mapping(self) -> dict[str, str]:
56
+ """The source mapping to expose to the debugger."""
57
+
58
+
59
+ @dataclasses.dataclass(frozen=True, kw_only=True)
60
+ class DebuggerSettings(metaclass=abc.ABCMeta):
61
+ """Common debugger settings."""
62
+
63
+ port: int = 5678
64
+ """
65
+ The port on the origin host which is listening for incoming connections from the debugger.
66
+ SSH port forwarding will be automatically configured for non-local hosts to connect to this port as needed.
67
+ """
68
+
69
+ def as_dict(self) -> dict[str, object]:
70
+ """Convert this instance to a dict."""
71
+ data = dataclasses.asdict(self)
72
+ data.update(__type__=self.__class__.__name__)
73
+
74
+ return data
75
+
76
+ @classmethod
77
+ def from_dict(cls, value: dict[str, t.Any]) -> t.Self:
78
+ """Load an instance from a dict."""
79
+ debug_cls = globals()[value.pop('__type__')]
80
+
81
+ return debug_cls(**value)
82
+
83
+ @classmethod
84
+ def get_debug_type(cls) -> str:
85
+ """Return the name for this debugger."""
86
+ return cls.__name__.removesuffix('Settings').lower()
87
+
88
+ @classmethod
89
+ def get_config_env_var_name(cls) -> str:
90
+ """Return the name of the environment variable used to customize settings for this debugger."""
91
+ return f'ANSIBLE_TEST_REMOTE_DEBUGGER_{cls.get_debug_type().upper()}'
92
+
93
+ @classmethod
94
+ def parse(cls, value: str) -> t.Self:
95
+ """Parse debugger settings from the given JSON and apply defaults."""
96
+ try:
97
+ settings = cls(**json.loads(value))
98
+ except Exception as ex:
99
+ raise ApplicationError(f"Invalid {cls.get_debug_type()} settings: {ex}") from ex
100
+
101
+ return cls.apply_defaults(settings)
102
+
103
+ @classmethod
104
+ @abc.abstractmethod
105
+ def is_active(cls) -> bool:
106
+ """Detect if the debugger is active."""
107
+
108
+ @classmethod
109
+ @abc.abstractmethod
110
+ def apply_defaults(cls, settings: t.Self) -> t.Self:
111
+ """Apply defaults to the given settings."""
112
+
113
+ @abc.abstractmethod
114
+ def get_python_package(self) -> str:
115
+ """The Python package to install for debugging."""
116
+
117
+ @abc.abstractmethod
118
+ def activate_debugger(self, profile: DebuggerProfile) -> None:
119
+ """Activate the debugger in ansible-test after delegation."""
120
+
121
+ @abc.abstractmethod
122
+ def get_ansiballz_config(self, profile: DebuggerProfile) -> dict[str, object]:
123
+ """Gets the extra configuration data for the AnsiballZ extension module."""
124
+
125
+ @abc.abstractmethod
126
+ def get_cli_arguments(self, profile: DebuggerProfile) -> list[str]:
127
+ """Get command line arguments for the debugger when running Ansible CLI programs."""
128
+
129
+ @abc.abstractmethod
130
+ def get_environment_variables(self, profile: DebuggerProfile) -> dict[str, str]:
131
+ """Get environment variables needed to configure the debugger for debugging."""
132
+
133
+
134
+ @dataclasses.dataclass(frozen=True, kw_only=True)
135
+ class PydevdSettings(DebuggerSettings):
136
+ """Settings for the pydevd debugger."""
137
+
138
+ package: str | None = None
139
+ """
140
+ The Python package to install for debugging.
141
+ If `None` then the package will be auto-detected.
142
+ If an empty string, then no package will be installed.
143
+ """
144
+
145
+ module: str | None = None
146
+ """
147
+ The Python module to import for debugging.
148
+ This should be pydevd or a derivative.
149
+ If not provided it will be auto-detected.
150
+ """
151
+
152
+ settrace: dict[str, object] = dataclasses.field(default_factory=dict)
153
+ """
154
+ Options to pass to the `{module}.settrace` method.
155
+ Used for running AnsiballZ modules only.
156
+ The `host` and `port` options will be provided by ansible-test.
157
+ The `suspend` option defaults to `False`.
158
+ """
159
+
160
+ args: list[str] = dataclasses.field(default_factory=list)
161
+ """
162
+ Arguments to pass to `pydevd` on the command line.
163
+ Used for running Ansible CLI programs only.
164
+ The `--client` and `--port` options will be provided by ansible-test.
165
+ """
166
+
167
+ @classmethod
168
+ def is_active(cls) -> bool:
169
+ return detect_pydevd_port() is not None
170
+
171
+ @classmethod
172
+ def apply_defaults(cls, settings: t.Self) -> t.Self:
173
+ if not settings.module:
174
+ if not settings.package or 'pydevd-pycharm' in settings.package:
175
+ module = 'pydevd_pycharm'
176
+ else:
177
+ module = 'pydevd'
50
178
 
51
- def parse_debugger_settings(value: str) -> DebuggerSettings:
52
- """Parse remote debugger settings and apply defaults."""
53
- try:
54
- settings = DebuggerSettings(**json.loads(value))
55
- except Exception as ex:
56
- raise ApplicationError(f"Invalid debugger settings: {ex}") from ex
179
+ settings = dataclasses.replace(settings, module=module)
57
180
 
58
- if not settings.module:
59
- if not settings.package or 'pydevd-pycharm' in settings.package:
60
- module = 'pydevd_pycharm'
181
+ if settings.package is None:
182
+ if settings.module == 'pydevd_pycharm':
183
+ if pycharm_version := detect_pycharm_version():
184
+ package = f'pydevd-pycharm~={pycharm_version}'
185
+ else:
186
+ package = None
187
+ else:
188
+ package = 'pydevd'
189
+
190
+ settings = dataclasses.replace(settings, package=package)
191
+
192
+ settings.settrace.setdefault('suspend', False)
193
+
194
+ if port := detect_pydevd_port():
195
+ settings = dataclasses.replace(settings, port=port)
196
+
197
+ if detect_pycharm_process():
198
+ # This only works with the default PyCharm debugger.
199
+ # Using it with PyCharm's "Python Debug Server" results in hangs in Ansible workers.
200
+ # Further investigation is required to understand the cause.
201
+ settings = dataclasses.replace(settings, args=settings.args + ['--multiprocess'])
202
+
203
+ return settings
204
+
205
+ def get_python_package(self) -> str:
206
+ if self.package is None and self.module == 'pydevd_pycharm':
207
+ display.warning('Skipping installation of `pydevd-pycharm` since the running PyCharm version was not detected.')
208
+
209
+ return self.package
210
+
211
+ def activate_debugger(self, profile: DebuggerProfile) -> None:
212
+ debugging_module = importlib.import_module(self.module)
213
+ debugging_module.settrace(**self._get_settrace_arguments(profile))
214
+
215
+ def get_ansiballz_config(self, profile: DebuggerProfile) -> dict[str, object]:
216
+ return dict(
217
+ module=self.module,
218
+ settrace=self._get_settrace_arguments(profile),
219
+ source_mapping=profile.get_source_mapping(),
220
+ )
221
+
222
+ def get_cli_arguments(self, profile: DebuggerProfile) -> list[str]:
223
+ # Although `pydevd_pycharm` can be used to invoke `settrace`, it cannot be used to run the debugger on the command line.
224
+ return ['-m', 'pydevd', '--client', profile.debugger_host, '--port', str(profile.debugger_port)] + self.args + ['--file']
225
+
226
+ def get_environment_variables(self, profile: DebuggerProfile) -> dict[str, str]:
227
+ return dict(
228
+ PATHS_FROM_ECLIPSE_TO_PYTHON=json.dumps(list(profile.get_source_mapping().items())),
229
+ PYDEVD_DISABLE_FILE_VALIDATION="1",
230
+ )
231
+
232
+ def _get_settrace_arguments(self, profile: DebuggerProfile) -> dict[str, object]:
233
+ """Get settrace arguments for pydevd."""
234
+ return self.settrace | dict(
235
+ host=profile.debugger_host,
236
+ port=profile.debugger_port,
237
+ )
238
+
239
+
240
+ @dataclasses.dataclass(frozen=True, kw_only=True)
241
+ class DebugpySettings(DebuggerSettings):
242
+ """Settings for the debugpy debugger."""
243
+
244
+ connect: dict[str, object] = dataclasses.field(default_factory=dict)
245
+ """
246
+ Options to pass to the `debugpy.connect` method.
247
+ Used for running AnsiballZ modules and ansible-test after delegation.
248
+ The endpoint addr, `access_token`, and `parent_session_pid` options will be provided by ansible-test.
249
+ """
250
+
251
+ args: list[str] = dataclasses.field(default_factory=list)
252
+ """
253
+ Arguments to pass to `debugpy` on the command line.
254
+ Used for running Ansible CLI programs only.
255
+ The `--connect`, `--adapter-access-token`, and `--parent-session-pid` options will be provided by ansible-test.
256
+ """
257
+
258
+ @classmethod
259
+ def is_active(cls) -> bool:
260
+ return detect_debugpy_options() is not None
261
+
262
+ @classmethod
263
+ def apply_defaults(cls, settings: t.Self) -> t.Self:
264
+ if options := detect_debugpy_options():
265
+ settings = dataclasses.replace(settings, port=options.port)
266
+ settings.connect.update(
267
+ access_token=options.adapter_access_token,
268
+ parent_session_pid=os.getpid(),
269
+ )
61
270
  else:
62
- module = 'pydevd'
271
+ display.warning('Debugging will be limited to the first connection. Run ansible-test under debugpy to support multiple connections.')
63
272
 
64
- settings = dataclasses.replace(settings, module=module)
273
+ return settings
65
274
 
66
- if settings.package is None:
67
- if settings.module == 'pydevd_pycharm':
68
- if pycharm_version := detect_pycharm_version():
69
- package = f'pydevd-pycharm~={pycharm_version}'
70
- else:
71
- package = None
72
- else:
73
- package = 'pydevd'
275
+ def get_python_package(self) -> str:
276
+ return 'debugpy'
277
+
278
+ def activate_debugger(self, profile: DebuggerProfile) -> None:
279
+ import debugpy # pylint: disable=import-error
280
+
281
+ debugpy.connect((profile.debugger_host, profile.debugger_port), **self.connect)
282
+
283
+ def get_ansiballz_config(self, profile: DebuggerProfile) -> dict[str, object]:
284
+ return dict(
285
+ host=profile.debugger_host,
286
+ port=profile.debugger_port,
287
+ connect=self.connect,
288
+ source_mapping=profile.get_source_mapping(),
289
+ )
290
+
291
+ def get_cli_arguments(self, profile: DebuggerProfile) -> list[str]:
292
+ cli_args = ['-m', 'debugpy', '--connect', f"{profile.debugger_host}:{profile.debugger_port}"]
293
+
294
+ if access_token := self.connect.get('access_token'):
295
+ cli_args += ['--adapter-access-token', str(access_token)]
296
+
297
+ if session_pid := self.connect.get('parent_session_pid'):
298
+ cli_args += ['--parent-session-pid', str(session_pid)]
299
+
300
+ if self.args:
301
+ cli_args += self.args
74
302
 
75
- settings = dataclasses.replace(settings, package=package)
303
+ return cli_args
76
304
 
77
- settings.settrace.setdefault('suspend', False)
305
+ def get_environment_variables(self, profile: DebuggerProfile) -> dict[str, str]:
306
+ return dict(
307
+ PATHS_FROM_ECLIPSE_TO_PYTHON=json.dumps(list(profile.get_source_mapping().items())),
308
+ PYDEVD_DISABLE_FILE_VALIDATION="1",
309
+ )
78
310
 
79
- if port := detect_pydevd_port():
80
- settings = dataclasses.replace(settings, port=port)
81
311
 
82
- if detect_pycharm_process():
83
- # This only works with the default PyCharm debugger.
84
- # Using it with PyCharm's "Python Debug Server" results in hangs in Ansible workers.
85
- # Further investigation is required to understand the cause.
86
- settings = dataclasses.replace(settings, args=settings.args + ['--multiprocess'])
312
+ def initialize_debugger(args: CommonConfig) -> None:
313
+ """Initialize the debugger settings before delegation."""
314
+ if not isinstance(args, EnvironmentConfig):
315
+ return
87
316
 
88
- return settings
317
+ if args.metadata.loaded:
318
+ return # after delegation
319
+
320
+ if collection := data_context().content.collection:
321
+ args.metadata.collection_root = collection.root
322
+
323
+ load_debugger_settings(args)
89
324
 
90
325
 
91
326
  def load_debugger_settings(args: EnvironmentConfig) -> None:
92
327
  """Load the remote debugger settings."""
328
+ use_debugger: type[DebuggerSettings] | None = None
329
+
93
330
  if args.metadata.debugger_flags.on_demand:
94
331
  # On-demand debugging only enables debugging if we're running under a debugger, otherwise it's a no-op.
95
332
 
96
- if not detect_pydevd_port():
333
+ for candidate_debugger in get_subclasses(DebuggerSettings):
334
+ if candidate_debugger.is_active():
335
+ use_debugger = candidate_debugger
336
+ break
337
+ else:
97
338
  display.info('Debugging disabled because no debugger was detected.', verbosity=1)
98
339
  args.metadata.debugger_flags = DebuggerFlags.all(False)
99
340
  return
@@ -107,13 +348,22 @@ def load_debugger_settings(args: EnvironmentConfig) -> None:
107
348
  if not args.metadata.debugger_flags.enable:
108
349
  return
109
350
 
110
- value = os.environ.get('ANSIBLE_TEST_REMOTE_DEBUGGER') or '{}'
111
- settings = parse_debugger_settings(value)
112
-
113
- display.info(f'>>> Debugger Settings\n{json.dumps(dataclasses.asdict(settings), indent=4)}', verbosity=3)
351
+ if not use_debugger: # detect debug type based on env var
352
+ for candidate_debugger in get_subclasses(DebuggerSettings):
353
+ if candidate_debugger.get_config_env_var_name() in os.environ:
354
+ use_debugger = candidate_debugger
355
+ break
356
+ else:
357
+ display.info('Debugging disabled because no debugger configuration was provided.', verbosity=1)
358
+ args.metadata.debugger_flags = DebuggerFlags.all(False)
359
+ return
114
360
 
361
+ config = os.environ.get(use_debugger.get_config_env_var_name()) or '{}'
362
+ settings = use_debugger.parse(config)
115
363
  args.metadata.debugger_settings = settings
116
364
 
365
+ display.info(f'>>> Debugger Settings ({use_debugger.get_debug_type()})\n{json.dumps(dataclasses.asdict(settings), indent=4)}', verbosity=3)
366
+
117
367
 
118
368
  @cache
119
369
  def detect_pydevd_port() -> int | None:
@@ -140,8 +390,6 @@ def detect_pycharm_version() -> str | None:
140
390
  display.info(f'Detected PyCharm version {version}.', verbosity=1)
141
391
  return version
142
392
 
143
- display.warning('Skipping installation of `pydevd-pycharm` since the running PyCharm version could not be detected.')
144
-
145
393
  return None
146
394
 
147
395
 
@@ -164,3 +412,43 @@ def detect_pycharm_process() -> Process | None:
164
412
  def get_current_process_cached() -> Process:
165
413
  """Return the current process. The result is cached."""
166
414
  return get_current_process()
415
+
416
+
417
+ @dataclasses.dataclass(frozen=True, kw_only=True)
418
+ class DebugpyOptions:
419
+ """Options detected from the debugpy instance hosting this process."""
420
+
421
+ port: int
422
+ adapter_access_token: str | None
423
+
424
+
425
+ @cache
426
+ def detect_debugpy_options() -> DebugpyOptions | None:
427
+ """Return the options for the debugpy instance hosting this process, or `None` if not detected."""
428
+ if "debugpy" not in sys.modules:
429
+ return None
430
+
431
+ import debugpy # pylint: disable=import-error
432
+
433
+ # get_cli_options is the new public API introduced after debugpy 1.8.15.
434
+ # We should remove the debugpy.server cli fallback once the new version is
435
+ # released.
436
+ if hasattr(debugpy, 'get_cli_options'):
437
+ opts = debugpy.get_cli_options()
438
+ else:
439
+ from debugpy.server import cli # pylint: disable=import-error
440
+ opts = cli.options
441
+
442
+ # address can be None if the debugger is not configured through the CLI as
443
+ # we expected.
444
+ if not opts.address:
445
+ return None
446
+
447
+ port = opts.address[1]
448
+
449
+ display.info(f'Detected debugpy debugger port {port}.', verbosity=1)
450
+
451
+ return DebugpyOptions(
452
+ port=port,
453
+ adapter_access_token=opts.adapter_access_token,
454
+ )
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import abc
6
6
  import dataclasses
7
- import importlib
8
7
  import json
9
8
  import os
10
9
  import pathlib
@@ -140,6 +139,11 @@ from .dev.container_probe import (
140
139
  check_container_cgroup_status,
141
140
  )
142
141
 
142
+ from .debugging import (
143
+ DebuggerProfile,
144
+ DebuggerSettings,
145
+ )
146
+
143
147
  TControllerHostConfig = t.TypeVar('TControllerHostConfig', bound=ControllerHostConfig)
144
148
  THostConfig = t.TypeVar('THostConfig', bound=HostConfig)
145
149
  TPosixConfig = t.TypeVar('TPosixConfig', bound=PosixConfig)
@@ -292,12 +296,17 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
292
296
  return f'{self.__class__.__name__}: {self.name}'
293
297
 
294
298
 
295
- class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
299
+ class DebuggableProfile(HostProfile[THostConfig], DebuggerProfile, metaclass=abc.ABCMeta):
296
300
  """Base class for profiles remote debugging."""
297
301
 
298
- __PYDEVD_PORT_KEY = 'pydevd_port'
302
+ __DEBUGGING_PORT_KEY = 'debugging_port'
299
303
  __DEBUGGING_FORWARDER_KEY = 'debugging_forwarder'
300
304
 
305
+ @property
306
+ def debugger(self) -> DebuggerSettings | None:
307
+ """The debugger settings for this host if present and enabled, otherwise None."""
308
+ return self.args.metadata.debugger_settings
309
+
301
310
  @property
302
311
  def debugging_enabled(self) -> bool:
303
312
  """Returns `True` if debugging is enabled for this profile, otherwise `False`."""
@@ -307,9 +316,14 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
307
316
  return self.args.metadata.debugger_flags.ansiballz
308
317
 
309
318
  @property
310
- def pydevd_port(self) -> int:
311
- """The pydevd port to use."""
312
- return self.state.get(self.__PYDEVD_PORT_KEY) or self.origin_pydev_port
319
+ def debugger_host(self) -> str:
320
+ """The debugger host to use."""
321
+ return 'localhost'
322
+
323
+ @property
324
+ def debugger_port(self) -> int:
325
+ """The debugger port to use."""
326
+ return self.state.get(self.__DEBUGGING_PORT_KEY) or self.origin_debugger_port
313
327
 
314
328
  @property
315
329
  def debugging_forwarder(self) -> SshProcess | None:
@@ -322,23 +336,23 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
322
336
  self.cache[self.__DEBUGGING_FORWARDER_KEY] = value
323
337
 
324
338
  @property
325
- def origin_pydev_port(self) -> int:
326
- """The pydevd port on the origin."""
327
- return self.args.metadata.debugger_settings.port
339
+ def origin_debugger_port(self) -> int:
340
+ """The debugger port on the origin."""
341
+ return self.debugger.port
328
342
 
329
343
  def enable_debugger_forwarding(self, ssh: SshConnectionDetail) -> None:
330
- """Enable pydevd port forwarding from the origin."""
344
+ """Enable debugger port forwarding from the origin."""
331
345
  if not self.debugging_enabled:
332
346
  return
333
347
 
334
- endpoint = ('localhost', self.origin_pydev_port)
348
+ endpoint = ('localhost', self.origin_debugger_port)
335
349
  forwards = [endpoint]
336
350
 
337
351
  self.debugging_forwarder = create_ssh_port_forwards(self.args, ssh, forwards)
338
352
 
339
353
  port_forwards = self.debugging_forwarder.collect_port_forwards()
340
354
 
341
- self.state[self.__PYDEVD_PORT_KEY] = port = port_forwards[endpoint]
355
+ self.state[self.__DEBUGGING_PORT_KEY] = port = port_forwards[endpoint]
342
356
 
343
357
  display.info(f'Remote debugging of {self.name!r} is available on port {port}.', verbosity=1)
344
358
 
@@ -355,19 +369,6 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
355
369
 
356
370
  self.debugging_forwarder.wait()
357
371
 
358
- def get_pydevd_settrace_arguments(self) -> dict[str, object]:
359
- """Get settrace arguments for pydevd."""
360
- return self.args.metadata.debugger_settings.settrace | dict(
361
- host="localhost",
362
- port=self.pydevd_port,
363
- )
364
-
365
- def get_pydevd_environment_variables(self) -> dict[str, str]:
366
- """Get environment variables needed to configure pydevd for debugging."""
367
- return dict(
368
- PATHS_FROM_ECLIPSE_TO_PYTHON=json.dumps(list(self.get_source_mapping().items())),
369
- )
370
-
371
372
  def get_source_mapping(self) -> dict[str, str]:
372
373
  """Get the source mapping from the given metadata."""
373
374
  from . import data_context
@@ -396,10 +397,9 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
396
397
 
397
398
  display.info('Activating remote debugging of ansible-test.', verbosity=1)
398
399
 
399
- os.environ.update(self.get_pydevd_environment_variables())
400
+ os.environ.update(self.debugger.get_environment_variables(self))
400
401
 
401
- debugging_module = importlib.import_module(self.args.metadata.debugger_settings.module)
402
- debugging_module.settrace(**self.get_pydevd_settrace_arguments())
402
+ self.debugger.activate_debugger(self)
403
403
 
404
404
  pass # pylint: disable=unnecessary-pass # when suspend is True, execution pauses here -- it's also a convenient place to put a breakpoint
405
405
 
@@ -411,9 +411,11 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
411
411
  if not self.args.metadata.debugger_flags.ansiballz:
412
412
  return {}
413
413
 
414
- return dict(
415
- _ansible_ansiballz_debugger_config=json.dumps(self.get_ansiballz_debugger_config()),
416
- )
414
+ debug_type = self.debugger.get_debug_type()
415
+
416
+ return {
417
+ f"_ansible_ansiballz_{debug_type}_config": json.dumps(self.get_ansiballz_debugger_config()),
418
+ }
417
419
 
418
420
  def get_ansiballz_environment_variables(self) -> dict[str, t.Any]:
419
421
  """
@@ -423,20 +425,18 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
423
425
  if not self.args.metadata.debugger_flags.ansiballz:
424
426
  return {}
425
427
 
426
- return dict(
427
- _ANSIBLE_ANSIBALLZ_DEBUGGER_CONFIG=json.dumps(self.get_ansiballz_debugger_config()),
428
- )
428
+ debug_type = self.debugger.get_debug_type().upper()
429
+
430
+ return {
431
+ f"_ANSIBLE_ANSIBALLZ_{debug_type}_CONFIG": json.dumps(self.get_ansiballz_debugger_config()),
432
+ }
429
433
 
430
434
  def get_ansiballz_debugger_config(self) -> dict[str, t.Any]:
431
435
  """
432
436
  Return config for remote debugging of AnsiballZ modules.
433
437
  When delegating, this function must be called after delegation.
434
438
  """
435
- debugger_config = dict(
436
- module=self.args.metadata.debugger_settings.module,
437
- settrace=self.get_pydevd_settrace_arguments(),
438
- source_mapping=self.get_source_mapping(),
439
- )
439
+ debugger_config = self.debugger.get_ansiballz_config(self)
440
440
 
441
441
  display.info(f'>>> Debugger Config ({self.name} AnsiballZ)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
442
442
 
@@ -451,8 +451,8 @@ class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
451
451
  return {}
452
452
 
453
453
  debugger_config = dict(
454
- args=['-m', 'pydevd', '--client', 'localhost', '--port', str(self.pydevd_port)] + self.args.metadata.debugger_settings.args + ['--file'],
455
- env=self.get_pydevd_environment_variables(),
454
+ args=self.debugger.get_cli_arguments(self),
455
+ env=self.debugger.get_environment_variables(self),
456
456
  )
457
457
 
458
458
  display.info(f'>>> Debugger Config ({self.name} Ansible CLI)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
@@ -597,9 +597,9 @@ class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[Con
597
597
  return self.controller_profile.name
598
598
 
599
599
  @property
600
- def pydevd_port(self) -> int:
600
+ def debugger_port(self) -> int:
601
601
  """The pydevd port to use."""
602
- return self.controller_profile.pydevd_port
602
+ return self.controller_profile.debugger_port
603
603
 
604
604
  def get_controller_target_connections(self) -> list[SshConnection]:
605
605
  """Return SSH connection(s) for accessing the host as a target from the controller."""