ansible-core 2.19.0rc2__py3-none-any.whl → 2.19.1rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansible-core might be problematic. Click here for more details.
- ansible/_internal/_ansiballz/_builder.py +25 -14
- ansible/_internal/_templating/_engine.py +6 -4
- ansible/_internal/_templating/_jinja_bits.py +3 -1
- ansible/_internal/_templating/_jinja_plugins.py +7 -2
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/config/base.yml +16 -6
- ansible/config/manager.py +7 -3
- ansible/executor/task_executor.py +4 -1
- ansible/executor/task_queue_manager.py +2 -2
- ansible/module_utils/_internal/_ansiballz/_extensions/_debugpy.py +97 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +2 -4
- ansible/module_utils/_internal/_traceback.py +1 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +10 -2
- ansible/module_utils/common/validation.py +4 -1
- ansible/modules/dnf.py +36 -50
- ansible/modules/dnf5.py +36 -29
- ansible/modules/meta.py +2 -1
- ansible/modules/service_facts.py +5 -1
- ansible/playbook/helpers.py +1 -0
- ansible/playbook/taggable.py +1 -2
- ansible/plugins/__init__.py +18 -10
- ansible/plugins/callback/__init__.py +6 -1
- ansible/plugins/lookup/template.py +6 -1
- ansible/release.py +1 -1
- ansible/utils/encrypt.py +2 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/RECORD +46 -45
- ansible_test/_internal/commands/integration/coverage.py +2 -2
- ansible_test/_internal/commands/shell/__init__.py +67 -28
- ansible_test/_internal/coverage_util.py +28 -25
- ansible_test/_internal/debugging.py +337 -49
- ansible_test/_internal/host_profiles.py +43 -43
- ansible_test/_internal/metadata.py +7 -42
- ansible_test/_internal/python_requirements.py +2 -2
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/target/setup/bootstrap.sh +37 -16
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0rc2.dist-info → ansible_core-2.19.1rc1.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
|
-
|
|
38
|
-
"""
|
|
39
|
-
if not isinstance(args, EnvironmentConfig):
|
|
40
|
-
return
|
|
44
|
+
class DebuggerProfile(t.Protocol):
|
|
45
|
+
"""Protocol for debugger profiles."""
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
@property
|
|
48
|
+
def debugger_host(self) -> str:
|
|
49
|
+
"""The hostname to expose to the debugger."""
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
271
|
+
display.warning('Debugging will be limited to the first connection. Run ansible-test under debugpy to support multiple connections.')
|
|
63
272
|
|
|
64
|
-
|
|
273
|
+
return settings
|
|
65
274
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
303
|
+
return cli_args
|
|
76
304
|
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
|
311
|
-
"""The
|
|
312
|
-
return
|
|
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
|
|
326
|
-
"""The
|
|
327
|
-
return self.
|
|
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
|
|
344
|
+
"""Enable debugger port forwarding from the origin."""
|
|
331
345
|
if not self.debugging_enabled:
|
|
332
346
|
return
|
|
333
347
|
|
|
334
|
-
endpoint = ('localhost', self.
|
|
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.
|
|
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.
|
|
400
|
+
os.environ.update(self.debugger.get_environment_variables(self))
|
|
400
401
|
|
|
401
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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 =
|
|
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=
|
|
455
|
-
env=self.
|
|
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
|
|
600
|
+
def debugger_port(self) -> int:
|
|
601
601
|
"""The pydevd port to use."""
|
|
602
|
-
return self.controller_profile.
|
|
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."""
|