ansible-core 2.19.0b5__py3-none-any.whl → 2.19.0b7__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.
- ansible/_internal/_ansiballz/__init__.py +0 -0
- ansible/_internal/_ansiballz/_builder.py +101 -0
- ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
- ansible/_internal/_templating/_jinja_bits.py +22 -4
- ansible/_internal/_templating/_jinja_common.py +1 -1
- ansible/_internal/_templating/_jinja_plugins.py +5 -2
- ansible/_internal/_templating/_template_vars.py +72 -0
- ansible/_internal/_templating/_transform.py +6 -0
- ansible/_internal/_yaml/_constructor.py +4 -4
- ansible/_internal/_yaml/_dumper.py +26 -18
- ansible/cli/__init__.py +9 -14
- ansible/cli/adhoc.py +6 -3
- ansible/cli/arguments/option_helpers.py +1 -1
- ansible/cli/console.py +2 -2
- ansible/cli/doc.py +4 -4
- ansible/cli/inventory.py +5 -7
- ansible/config/base.yml +33 -6
- ansible/errors/__init__.py +2 -1
- ansible/executor/module_common.py +75 -44
- ansible/executor/powershell/psrp_put_file.ps1 +1 -1
- ansible/executor/process/worker.py +2 -2
- ansible/executor/task_executor.py +2 -2
- ansible/executor/task_queue_manager.py +34 -70
- ansible/executor/task_result.py +1 -1
- ansible/galaxy/api.py +3 -6
- ansible/galaxy/collection/__init__.py +1 -6
- ansible/galaxy/collection/concrete_artifact_manager.py +4 -10
- ansible/galaxy/dependency_resolution/providers.py +3 -3
- ansible/galaxy/role.py +2 -2
- ansible/inventory/group.py +6 -1
- ansible/inventory/host.py +6 -1
- ansible/module_utils/_internal/__init__.py +7 -4
- ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
- ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +10 -38
- ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
- ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
- ansible/module_utils/_internal/_datatag/__init__.py +23 -1
- ansible/module_utils/_internal/_deprecator.py +39 -34
- ansible/module_utils/_internal/_json/_profiles/__init__.py +1 -0
- ansible/module_utils/_internal/_messages.py +26 -2
- ansible/module_utils/_internal/_plugin_info.py +14 -1
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +58 -70
- ansible/module_utils/common/respawn.py +4 -41
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/connection.py +8 -11
- ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
- ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
- ansible/module_utils/facts/hardware/base.py +1 -1
- ansible/module_utils/facts/hardware/linux.py +1 -1
- ansible/module_utils/facts/other/facter.py +1 -1
- ansible/module_utils/facts/sysctl.py +4 -6
- ansible/module_utils/facts/system/caps.py +2 -2
- ansible/module_utils/facts/system/distribution.py +2 -2
- ansible/module_utils/facts/system/local.py +1 -1
- ansible/module_utils/facts/virtual/linux.py +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
- ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
- ansible/module_utils/service.py +1 -1
- ansible/module_utils/urls.py +5 -5
- ansible/modules/apt.py +9 -3
- ansible/modules/apt_repository.py +10 -10
- ansible/modules/assemble.py +7 -5
- ansible/modules/async_wrapper.py +7 -17
- ansible/modules/command.py +3 -3
- ansible/modules/copy.py +4 -4
- ansible/modules/cron.py +1 -1
- ansible/modules/expect.py +5 -5
- ansible/modules/file.py +16 -17
- ansible/modules/find.py +3 -3
- ansible/modules/get_url.py +17 -0
- ansible/modules/git.py +9 -7
- ansible/modules/hostname.py +2 -2
- ansible/modules/known_hosts.py +12 -14
- ansible/modules/package.py +6 -0
- ansible/modules/pip.py +9 -11
- ansible/modules/raw.py +2 -2
- ansible/modules/replace.py +2 -2
- ansible/modules/slurp.py +10 -13
- ansible/modules/stat.py +6 -8
- ansible/modules/unarchive.py +6 -6
- ansible/modules/user.py +1 -1
- ansible/modules/wait_for.py +38 -33
- ansible/modules/yum_repository.py +4 -3
- ansible/parsing/dataloader.py +2 -2
- ansible/parsing/mod_args.py +38 -20
- ansible/parsing/vault/__init__.py +9 -13
- ansible/playbook/base.py +7 -4
- ansible/playbook/helpers.py +1 -1
- ansible/playbook/included_file.py +3 -1
- ansible/playbook/play_context.py +2 -0
- ansible/playbook/playbook_include.py +23 -56
- ansible/playbook/role/__init__.py +38 -21
- ansible/playbook/taggable.py +19 -5
- ansible/playbook/task.py +2 -0
- ansible/plugins/action/__init__.py +2 -2
- ansible/plugins/action/assemble.py +2 -1
- ansible/plugins/action/assert.py +2 -2
- ansible/plugins/action/fetch.py +3 -3
- ansible/plugins/action/script.py +5 -4
- ansible/plugins/action/template.py +9 -3
- ansible/plugins/cache/__init__.py +17 -19
- ansible/plugins/callback/__init__.py +77 -87
- ansible/plugins/callback/default.py +0 -3
- ansible/plugins/callback/junit.py +0 -6
- ansible/plugins/callback/tree.py +5 -5
- ansible/plugins/connection/local.py +4 -4
- ansible/plugins/connection/paramiko_ssh.py +5 -5
- ansible/plugins/connection/ssh.py +9 -7
- ansible/plugins/connection/winrm.py +1 -1
- ansible/plugins/filter/core.py +19 -21
- ansible/plugins/filter/encryption.py +10 -2
- ansible/plugins/filter/pow.yml +1 -1
- ansible/plugins/filter/root.yml +1 -1
- ansible/plugins/filter/strftime.yml +3 -3
- ansible/plugins/filter/to_uuid.yml +1 -1
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +5 -4
- ansible/plugins/loader.py +5 -0
- ansible/plugins/lookup/password.py +4 -6
- ansible/plugins/lookup/template.py +9 -4
- ansible/plugins/shell/powershell.py +3 -2
- ansible/plugins/shell/sh.py +3 -2
- ansible/plugins/strategy/__init__.py +3 -3
- ansible/plugins/test/core.py +2 -2
- ansible/release.py +1 -1
- ansible/template/__init__.py +9 -53
- ansible/utils/collection_loader/_collection_finder.py +3 -3
- ansible/utils/display.py +38 -37
- ansible/utils/galaxy.py +2 -2
- ansible/utils/hashing.py +6 -7
- ansible/utils/path.py +6 -8
- ansible/utils/py3compat.py +2 -1
- ansible/utils/ssh_functions.py +3 -2
- ansible/utils/vars.py +4 -1
- ansible/vars/manager.py +6 -3
- ansible/vars/plugins.py +3 -3
- ansible/vars/reserved.py +6 -4
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +184 -173
- ansible_test/_internal/__init__.py +5 -0
- ansible_test/_internal/ansible_util.py +1 -1
- ansible_test/_internal/classification/python.py +6 -0
- ansible_test/_internal/cli/commands/__init__.py +0 -5
- ansible_test/_internal/cli/environments.py +51 -5
- ansible_test/_internal/commands/coverage/__init__.py +1 -1
- ansible_test/_internal/commands/integration/__init__.py +18 -5
- ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
- ansible_test/_internal/commands/integration/coverage.py +7 -2
- ansible_test/_internal/commands/sanity/__init__.py +3 -1
- ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
- ansible_test/_internal/commands/shell/__init__.py +43 -4
- ansible_test/_internal/commands/units/__init__.py +4 -1
- ansible_test/_internal/config.py +21 -13
- ansible_test/_internal/debugging.py +166 -0
- ansible_test/_internal/delegation.py +21 -13
- ansible_test/_internal/host_profiles.py +259 -16
- ansible_test/_internal/inventory.py +4 -0
- ansible_test/_internal/metadata.py +94 -4
- ansible_test/_internal/processes.py +80 -0
- ansible_test/_internal/provisioning.py +10 -4
- ansible_test/_internal/python_requirements.py +27 -0
- ansible_test/_internal/ssh.py +1 -5
- ansible_test/_internal/target.py +8 -0
- ansible_test/_internal/thread.py +2 -1
- ansible_test/_internal/timeout.py +1 -1
- ansible_test/_internal/util.py +20 -12
- ansible_test/_internal/util_common.py +13 -3
- ansible_test/_util/target/injector/python.py +8 -0
- ansible_test/_util/target/setup/requirements.py +3 -9
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -113,23 +113,27 @@ def delegate(args: CommonConfig, host_state: HostState, exclude: list[str], requ
|
|
113
113
|
assert isinstance(args, EnvironmentConfig)
|
114
114
|
|
115
115
|
with delegation_context(args, host_state):
|
116
|
-
|
117
|
-
args.metadata.ci_provider = get_ci_provider().code
|
116
|
+
args.metadata.ci_provider = get_ci_provider().code
|
118
117
|
|
119
|
-
|
118
|
+
make_dirs(ResultType.TMP.path)
|
120
119
|
|
121
|
-
|
122
|
-
args.metadata_path = os.path.join(ResultType.TMP.relative_path, os.path.basename(metadata_fd.name))
|
123
|
-
args.metadata.to_file(args.metadata_path)
|
124
|
-
|
125
|
-
try:
|
126
|
-
delegate_command(args, host_state, exclude, require)
|
127
|
-
finally:
|
128
|
-
args.metadata_path = None
|
129
|
-
else:
|
120
|
+
with metadata_context(args):
|
130
121
|
delegate_command(args, host_state, exclude, require)
|
131
122
|
|
132
123
|
|
124
|
+
@contextlib.contextmanager
|
125
|
+
def metadata_context(args: EnvironmentConfig) -> t.Generator[None]:
|
126
|
+
"""A context manager which exports delegation metadata."""
|
127
|
+
with tempfile.NamedTemporaryFile(prefix='metadata-', suffix='.json', dir=ResultType.TMP.path) as metadata_fd:
|
128
|
+
args.metadata_path = os.path.join(ResultType.TMP.relative_path, os.path.basename(metadata_fd.name))
|
129
|
+
args.metadata.to_file(args.metadata_path)
|
130
|
+
|
131
|
+
try:
|
132
|
+
yield
|
133
|
+
finally:
|
134
|
+
args.metadata_path = None
|
135
|
+
|
136
|
+
|
133
137
|
def delegate_command(args: EnvironmentConfig, host_state: HostState, exclude: list[str], require: list[str]) -> None:
|
134
138
|
"""Delegate execution based on the provided host state."""
|
135
139
|
con = host_state.controller_profile.get_origin_controller_connection()
|
@@ -189,6 +193,10 @@ def delegate_command(args: EnvironmentConfig, host_state: HostState, exclude: li
|
|
189
193
|
networks = container.get_network_names()
|
190
194
|
|
191
195
|
if networks is not None:
|
196
|
+
if args.metadata.debugger_flags.enable:
|
197
|
+
networks = []
|
198
|
+
display.warning('Skipping network isolation to enable remote debugging.')
|
199
|
+
|
192
200
|
for network in networks:
|
193
201
|
try:
|
194
202
|
con.disconnect_network(network)
|
@@ -334,6 +342,7 @@ def filter_options(
|
|
334
342
|
('--redact', 0, False),
|
335
343
|
('--no-redact', 0, not args.redact),
|
336
344
|
('--host-path', 1, args.host_path),
|
345
|
+
('--metadata', 1, args.metadata_path),
|
337
346
|
]
|
338
347
|
|
339
348
|
if isinstance(args, TestConfig):
|
@@ -346,7 +355,6 @@ def filter_options(
|
|
346
355
|
('--ignore-unstaged', 0, False),
|
347
356
|
('--changed-from', 1, False),
|
348
357
|
('--changed-path', 1, False),
|
349
|
-
('--metadata', 1, args.metadata_path),
|
350
358
|
('--exclude', 1, exclude),
|
351
359
|
('--require', 1, require),
|
352
360
|
('--base-branch', 1, False),
|
@@ -4,7 +4,10 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import abc
|
6
6
|
import dataclasses
|
7
|
+
import importlib
|
8
|
+
import json
|
7
9
|
import os
|
10
|
+
import pathlib
|
8
11
|
import shlex
|
9
12
|
import tempfile
|
10
13
|
import time
|
@@ -58,6 +61,9 @@ from .util import (
|
|
58
61
|
HostConnectionError,
|
59
62
|
ANSIBLE_TEST_TARGET_ROOT,
|
60
63
|
WINDOWS_CONNECTION_VARIABLES,
|
64
|
+
ANSIBLE_SOURCE_ROOT,
|
65
|
+
ANSIBLE_LIB_ROOT,
|
66
|
+
ANSIBLE_TEST_ROOT,
|
61
67
|
)
|
62
68
|
|
63
69
|
from .util_common import (
|
@@ -92,6 +98,8 @@ from .venv import (
|
|
92
98
|
|
93
99
|
from .ssh import (
|
94
100
|
SshConnectionDetail,
|
101
|
+
create_ssh_port_forwards,
|
102
|
+
SshProcess,
|
95
103
|
)
|
96
104
|
|
97
105
|
from .ansible_util import (
|
@@ -206,7 +214,7 @@ class Inventory:
|
|
206
214
|
inventory_text += f'[{group}]\n'
|
207
215
|
|
208
216
|
for host, variables in hosts.items():
|
209
|
-
kvp = ' '.join(f
|
217
|
+
kvp = ' '.join(f"{key}={value!r}" for key, value in variables.items())
|
210
218
|
inventory_text += f'{host} {kvp}\n'
|
211
219
|
|
212
220
|
inventory_text += '\n'
|
@@ -235,18 +243,24 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
|
|
235
243
|
*,
|
236
244
|
args: EnvironmentConfig,
|
237
245
|
config: THostConfig,
|
238
|
-
|
246
|
+
controller: ControllerHostProfile,
|
239
247
|
) -> None:
|
240
248
|
self.args = args
|
241
249
|
self.config = config
|
242
|
-
self.controller =
|
243
|
-
self.targets = targets
|
250
|
+
self.controller = not controller # this profile is a controller whenever the `controller` arg was not provided
|
251
|
+
self.targets = args.targets if self.controller else [] # only keep targets if this profile is a controller
|
252
|
+
self.controller_profile = controller if isinstance(self, ControllerProfile) else None
|
244
253
|
|
245
254
|
self.state: dict[str, t.Any] = {}
|
246
255
|
"""State that must be persisted across delegation."""
|
247
256
|
self.cache: dict[str, t.Any] = {}
|
248
257
|
"""Cache that must not be persisted across delegation."""
|
249
258
|
|
259
|
+
@property
|
260
|
+
@abc.abstractmethod
|
261
|
+
def name(self) -> str:
|
262
|
+
"""The name of the host profile."""
|
263
|
+
|
250
264
|
def provision(self) -> None:
|
251
265
|
"""Provision the host before delegation."""
|
252
266
|
|
@@ -274,6 +288,179 @@ class HostProfile(t.Generic[THostConfig], metaclass=abc.ABCMeta):
|
|
274
288
|
# args will be populated after the instances are restored
|
275
289
|
self.cache = {}
|
276
290
|
|
291
|
+
def __str__(self) -> str:
|
292
|
+
return f'{self.__class__.__name__}: {self.name}'
|
293
|
+
|
294
|
+
|
295
|
+
class DebuggableProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
|
296
|
+
"""Base class for profiles remote debugging."""
|
297
|
+
|
298
|
+
__PYDEVD_PORT_KEY = 'pydevd_port'
|
299
|
+
__DEBUGGING_FORWARDER_KEY = 'debugging_forwarder'
|
300
|
+
|
301
|
+
@property
|
302
|
+
def debugging_enabled(self) -> bool:
|
303
|
+
"""Returns `True` if debugging is enabled for this profile, otherwise `False`."""
|
304
|
+
if self.controller:
|
305
|
+
return self.args.metadata.debugger_flags.enable
|
306
|
+
|
307
|
+
return self.args.metadata.debugger_flags.ansiballz
|
308
|
+
|
309
|
+
@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
|
313
|
+
|
314
|
+
@property
|
315
|
+
def debugging_forwarder(self) -> SshProcess | None:
|
316
|
+
"""The SSH forwarding process, if enabled."""
|
317
|
+
return self.cache.get(self.__DEBUGGING_FORWARDER_KEY)
|
318
|
+
|
319
|
+
@debugging_forwarder.setter
|
320
|
+
def debugging_forwarder(self, value: SshProcess) -> None:
|
321
|
+
"""The SSH forwarding process, if enabled."""
|
322
|
+
self.cache[self.__DEBUGGING_FORWARDER_KEY] = value
|
323
|
+
|
324
|
+
@property
|
325
|
+
def origin_pydev_port(self) -> int:
|
326
|
+
"""The pydevd port on the origin."""
|
327
|
+
return self.args.metadata.debugger_settings.port
|
328
|
+
|
329
|
+
def enable_debugger_forwarding(self, ssh: SshConnectionDetail) -> None:
|
330
|
+
"""Enable pydevd port forwarding from the origin."""
|
331
|
+
if not self.debugging_enabled:
|
332
|
+
return
|
333
|
+
|
334
|
+
endpoint = ('localhost', self.origin_pydev_port)
|
335
|
+
forwards = [endpoint]
|
336
|
+
|
337
|
+
self.debugging_forwarder = create_ssh_port_forwards(self.args, ssh, forwards)
|
338
|
+
|
339
|
+
port_forwards = self.debugging_forwarder.collect_port_forwards()
|
340
|
+
|
341
|
+
self.state[self.__PYDEVD_PORT_KEY] = port = port_forwards[endpoint]
|
342
|
+
|
343
|
+
display.info(f'Remote debugging of {self.name!r} is available on port {port}.', verbosity=1)
|
344
|
+
|
345
|
+
def deprovision(self) -> None:
|
346
|
+
"""Deprovision the host after delegation has completed."""
|
347
|
+
super().deprovision()
|
348
|
+
|
349
|
+
if not self.debugging_forwarder:
|
350
|
+
return # forwarding not in use
|
351
|
+
|
352
|
+
self.debugging_forwarder.terminate()
|
353
|
+
|
354
|
+
display.info(f'Waiting for the {self.name!r} remote debugging SSH port forwarding process to terminate.', verbosity=1)
|
355
|
+
|
356
|
+
self.debugging_forwarder.wait()
|
357
|
+
|
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
|
+
def get_source_mapping(self) -> dict[str, str]:
|
372
|
+
"""Get the source mapping from the given metadata."""
|
373
|
+
from . import data_context
|
374
|
+
|
375
|
+
if collection := data_context().content.collection:
|
376
|
+
source_mapping = {
|
377
|
+
f"{self.args.metadata.ansible_test_root}/": f'{ANSIBLE_TEST_ROOT}/',
|
378
|
+
f"{self.args.metadata.ansible_lib_root}/": f'{ANSIBLE_LIB_ROOT}/',
|
379
|
+
f'{self.args.metadata.collection_root}/ansible_collections/': f'{collection.root}/ansible_collections/',
|
380
|
+
}
|
381
|
+
else:
|
382
|
+
ansible_source_root = pathlib.Path(self.args.metadata.ansible_lib_root).parent.parent
|
383
|
+
|
384
|
+
source_mapping = {
|
385
|
+
f"{ansible_source_root}/": f'{ANSIBLE_SOURCE_ROOT}/',
|
386
|
+
}
|
387
|
+
|
388
|
+
source_mapping = {key: value for key, value in source_mapping.items() if key != value}
|
389
|
+
|
390
|
+
return source_mapping
|
391
|
+
|
392
|
+
def activate_debugger(self) -> None:
|
393
|
+
"""Activate the debugger after delegation."""
|
394
|
+
if not self.args.metadata.loaded or not self.args.metadata.debugger_flags.self:
|
395
|
+
return
|
396
|
+
|
397
|
+
display.info('Activating remote debugging of ansible-test.', verbosity=1)
|
398
|
+
|
399
|
+
os.environ.update(self.get_pydevd_environment_variables())
|
400
|
+
|
401
|
+
debugging_module = importlib.import_module(self.args.metadata.debugger_settings.module)
|
402
|
+
debugging_module.settrace(**self.get_pydevd_settrace_arguments())
|
403
|
+
|
404
|
+
pass # pylint: disable=unnecessary-pass # when suspend is True, execution pauses here -- it's also a convenient place to put a breakpoint
|
405
|
+
|
406
|
+
def get_ansiballz_inventory_variables(self) -> dict[str, t.Any]:
|
407
|
+
"""
|
408
|
+
Return inventory variables for remote debugging of AnsiballZ modules.
|
409
|
+
When delegating, this function must be called after delegation.
|
410
|
+
"""
|
411
|
+
if not self.args.metadata.debugger_flags.ansiballz:
|
412
|
+
return {}
|
413
|
+
|
414
|
+
return dict(
|
415
|
+
_ansible_ansiballz_debugger_config=json.dumps(self.get_ansiballz_debugger_config()),
|
416
|
+
)
|
417
|
+
|
418
|
+
def get_ansiballz_environment_variables(self) -> dict[str, t.Any]:
|
419
|
+
"""
|
420
|
+
Return environment variables for remote debugging of AnsiballZ modules.
|
421
|
+
When delegating, this function must be called after delegation.
|
422
|
+
"""
|
423
|
+
if not self.args.metadata.debugger_flags.ansiballz:
|
424
|
+
return {}
|
425
|
+
|
426
|
+
return dict(
|
427
|
+
_ANSIBLE_ANSIBALLZ_DEBUGGER_CONFIG=json.dumps(self.get_ansiballz_debugger_config()),
|
428
|
+
)
|
429
|
+
|
430
|
+
def get_ansiballz_debugger_config(self) -> dict[str, t.Any]:
|
431
|
+
"""
|
432
|
+
Return config for remote debugging of AnsiballZ modules.
|
433
|
+
When delegating, this function must be called after delegation.
|
434
|
+
"""
|
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
|
+
)
|
440
|
+
|
441
|
+
display.info(f'>>> Debugger Config ({self.name} AnsiballZ)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
|
442
|
+
|
443
|
+
return debugger_config
|
444
|
+
|
445
|
+
def get_ansible_cli_environment_variables(self) -> dict[str, t.Any]:
|
446
|
+
"""
|
447
|
+
Return environment variables for remote debugging of the Ansible CLI.
|
448
|
+
When delegating, this function must be called after delegation.
|
449
|
+
"""
|
450
|
+
if not self.args.metadata.debugger_flags.cli:
|
451
|
+
return {}
|
452
|
+
|
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(),
|
456
|
+
)
|
457
|
+
|
458
|
+
display.info(f'>>> Debugger Config ({self.name} Ansible CLI)\n{json.dumps(debugger_config, indent=4)}', verbosity=3)
|
459
|
+
|
460
|
+
return dict(
|
461
|
+
ANSIBLE_TEST_DEBUGGER_CONFIG=json.dumps(debugger_config),
|
462
|
+
)
|
463
|
+
|
277
464
|
|
278
465
|
class PosixProfile(HostProfile[TPosixConfig], metaclass=abc.ABCMeta):
|
279
466
|
"""Base class for POSIX host profiles."""
|
@@ -297,7 +484,7 @@ class PosixProfile(HostProfile[TPosixConfig], metaclass=abc.ABCMeta):
|
|
297
484
|
return python
|
298
485
|
|
299
486
|
|
300
|
-
class ControllerHostProfile(PosixProfile[TControllerHostConfig], metaclass=abc.ABCMeta):
|
487
|
+
class ControllerHostProfile(PosixProfile[TControllerHostConfig], DebuggableProfile[TControllerHostConfig], metaclass=abc.ABCMeta):
|
301
488
|
"""Base class for profiles usable as a controller."""
|
302
489
|
|
303
490
|
@abc.abstractmethod
|
@@ -320,6 +507,11 @@ class SshTargetHostProfile(HostProfile[THostConfig], metaclass=abc.ABCMeta):
|
|
320
507
|
class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
|
321
508
|
"""Base class for remote instance profiles."""
|
322
509
|
|
510
|
+
@property
|
511
|
+
def name(self) -> str:
|
512
|
+
"""The name of the host profile."""
|
513
|
+
return self.config.name
|
514
|
+
|
323
515
|
@property
|
324
516
|
def core_ci_state(self) -> t.Optional[dict[str, str]]:
|
325
517
|
"""The saved Ansible Core CI state."""
|
@@ -339,6 +531,8 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
|
|
339
531
|
|
340
532
|
def deprovision(self) -> None:
|
341
533
|
"""Deprovision the host after delegation has completed."""
|
534
|
+
super().deprovision()
|
535
|
+
|
342
536
|
if self.args.remote_terminate == TerminateMode.ALWAYS or (self.args.remote_terminate == TerminateMode.SUCCESS and self.args.success):
|
343
537
|
self.delete_instance()
|
344
538
|
|
@@ -394,9 +588,19 @@ class RemoteProfile(SshTargetHostProfile[TRemoteConfig], metaclass=abc.ABCMeta):
|
|
394
588
|
)
|
395
589
|
|
396
590
|
|
397
|
-
class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[ControllerConfig]):
|
591
|
+
class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[ControllerConfig], DebuggableProfile[ControllerConfig]):
|
398
592
|
"""Host profile for the controller as a target."""
|
399
593
|
|
594
|
+
@property
|
595
|
+
def name(self) -> str:
|
596
|
+
"""The name of the host profile."""
|
597
|
+
return self.controller_profile.name
|
598
|
+
|
599
|
+
@property
|
600
|
+
def pydevd_port(self) -> int:
|
601
|
+
"""The pydevd port to use."""
|
602
|
+
return self.controller_profile.pydevd_port
|
603
|
+
|
400
604
|
def get_controller_target_connections(self) -> list[SshConnection]:
|
401
605
|
"""Return SSH connection(s) for accessing the host as a target from the controller."""
|
402
606
|
settings = SshConnectionDetail(
|
@@ -411,7 +615,7 @@ class ControllerProfile(SshTargetHostProfile[ControllerConfig], PosixProfile[Con
|
|
411
615
|
return [SshConnection(self.args, settings)]
|
412
616
|
|
413
617
|
|
414
|
-
class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[DockerConfig]):
|
618
|
+
class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[DockerConfig], DebuggableProfile[DockerConfig]):
|
415
619
|
"""Host profile for a docker instance."""
|
416
620
|
|
417
621
|
MARKER = 'ansible-test-marker'
|
@@ -425,6 +629,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
425
629
|
command_privileged: bool
|
426
630
|
expected_mounts: tuple[CGroupMount, ...]
|
427
631
|
|
632
|
+
@property
|
633
|
+
def name(self) -> str:
|
634
|
+
"""The name of the host profile."""
|
635
|
+
return self.config.name
|
636
|
+
|
428
637
|
@property
|
429
638
|
def container_name(self) -> t.Optional[str]:
|
430
639
|
"""Return the stored container name, if any, otherwise None."""
|
@@ -461,7 +670,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
461
670
|
image=self.config.image,
|
462
671
|
name=f'ansible-test-{self.label}',
|
463
672
|
ports=[22],
|
464
|
-
publish_ports=not self.controller, #
|
673
|
+
publish_ports=self.debugging_enabled or not self.controller, # SSH to the controller is not required unless remote debugging is enabled
|
465
674
|
options=init_config.options,
|
466
675
|
cleanup=False,
|
467
676
|
cmd=self.build_init_command(init_config, init_probe),
|
@@ -601,7 +810,7 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
601
810
|
# The host namespace must be used to permit the container to access the cgroup v1 systemd hierarchy created by Podman.
|
602
811
|
'--cgroupns', 'host',
|
603
812
|
# Mask the host cgroup tmpfs mount to avoid exposing the host cgroup v1 hierarchies (or cgroup v2 hybrid) to the container.
|
604
|
-
# Podman will provide a cgroup v1 systemd
|
813
|
+
# Podman will provide a cgroup v1 systemd hierarchy on top of this.
|
605
814
|
'--tmpfs', '/sys/fs/cgroup',
|
606
815
|
))
|
607
816
|
|
@@ -974,8 +1183,13 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
974
1183
|
docker_logs(self.args, self.container_name)
|
975
1184
|
raise
|
976
1185
|
|
1186
|
+
if self.debugging_enabled:
|
1187
|
+
self.enable_debugger_forwarding(self.get_ssh_connection_detail(HostType.origin))
|
1188
|
+
|
977
1189
|
def deprovision(self) -> None:
|
978
1190
|
"""Deprovision the host after delegation has completed."""
|
1191
|
+
super().deprovision()
|
1192
|
+
|
979
1193
|
container_exists = False
|
980
1194
|
|
981
1195
|
if self.container_name:
|
@@ -1025,10 +1239,10 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
1025
1239
|
|
1026
1240
|
raise HostConnectionError(f'Timeout waiting for {self.config.name} container {self.container_name}.', callback)
|
1027
1241
|
|
1028
|
-
def
|
1029
|
-
"""Return SSH connection
|
1242
|
+
def get_ssh_connection_detail(self, host_type: str) -> SshConnectionDetail:
|
1243
|
+
"""Return SSH connection detail for the specified host type."""
|
1030
1244
|
containers = get_container_database(self.args)
|
1031
|
-
access = containers.data[
|
1245
|
+
access = containers.data[host_type]['__test_hosts__'][self.container_name]
|
1032
1246
|
|
1033
1247
|
host = access.host_ip
|
1034
1248
|
port = dict(access.port_map())[22]
|
@@ -1046,7 +1260,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
1046
1260
|
enable_rsa_sha1='centos6' in self.config.image,
|
1047
1261
|
)
|
1048
1262
|
|
1049
|
-
return
|
1263
|
+
return settings
|
1264
|
+
|
1265
|
+
def get_controller_target_connections(self) -> list[SshConnection]:
|
1266
|
+
"""Return SSH connection(s) for accessing the host as a target from the controller."""
|
1267
|
+
return [SshConnection(self.args, self.get_ssh_connection_detail(HostType.control))]
|
1050
1268
|
|
1051
1269
|
def get_origin_controller_connection(self) -> DockerConnection:
|
1052
1270
|
"""Return a connection for accessing the host as a controller from the origin."""
|
@@ -1116,6 +1334,11 @@ class DockerProfile(ControllerHostProfile[DockerConfig], SshTargetHostProfile[Do
|
|
1116
1334
|
class NetworkInventoryProfile(HostProfile[NetworkInventoryConfig]):
|
1117
1335
|
"""Host profile for a network inventory."""
|
1118
1336
|
|
1337
|
+
@property
|
1338
|
+
def name(self) -> str:
|
1339
|
+
"""The name of the host profile."""
|
1340
|
+
return self.config.path
|
1341
|
+
|
1119
1342
|
|
1120
1343
|
class NetworkRemoteProfile(RemoteProfile[NetworkRemoteConfig]):
|
1121
1344
|
"""Host profile for a network remote instance."""
|
@@ -1197,6 +1420,11 @@ class NetworkRemoteProfile(RemoteProfile[NetworkRemoteConfig]):
|
|
1197
1420
|
class OriginProfile(ControllerHostProfile[OriginConfig]):
|
1198
1421
|
"""Host profile for origin."""
|
1199
1422
|
|
1423
|
+
@property
|
1424
|
+
def name(self) -> str:
|
1425
|
+
"""The name of the host profile."""
|
1426
|
+
return 'origin'
|
1427
|
+
|
1200
1428
|
def get_origin_controller_connection(self) -> LocalConnection:
|
1201
1429
|
"""Return a connection for accessing the host as a controller from the origin."""
|
1202
1430
|
return LocalConnection(self.args)
|
@@ -1206,13 +1434,18 @@ class OriginProfile(ControllerHostProfile[OriginConfig]):
|
|
1206
1434
|
return os.getcwd()
|
1207
1435
|
|
1208
1436
|
|
1209
|
-
class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile[PosixRemoteConfig]):
|
1437
|
+
class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile[PosixRemoteConfig], DebuggableProfile[PosixRemoteConfig]):
|
1210
1438
|
"""Host profile for a POSIX remote instance."""
|
1211
1439
|
|
1212
1440
|
def wait(self) -> None:
|
1213
1441
|
"""Wait for the instance to be ready. Executed before delegation for the controller and after delegation for targets."""
|
1214
1442
|
self.wait_until_ready()
|
1215
1443
|
|
1444
|
+
def setup(self) -> None:
|
1445
|
+
"""Perform out-of-band setup before delegation."""
|
1446
|
+
if self.debugging_enabled:
|
1447
|
+
self.enable_debugger_forwarding(self.get_origin_controller_connection().settings)
|
1448
|
+
|
1216
1449
|
def configure(self) -> None:
|
1217
1450
|
"""Perform in-band configuration. Executed before delegation for the controller and after delegation for targets."""
|
1218
1451
|
# a target uses a single python version, but a controller may include additional versions for targets running on the controller
|
@@ -1317,6 +1550,11 @@ class PosixRemoteProfile(ControllerHostProfile[PosixRemoteConfig], RemoteProfile
|
|
1317
1550
|
class PosixSshProfile(SshTargetHostProfile[PosixSshConfig], PosixProfile[PosixSshConfig]):
|
1318
1551
|
"""Host profile for a POSIX SSH instance."""
|
1319
1552
|
|
1553
|
+
@property
|
1554
|
+
def name(self) -> str:
|
1555
|
+
"""The name of the host profile."""
|
1556
|
+
return self.config.host
|
1557
|
+
|
1320
1558
|
def get_controller_target_connections(self) -> list[SshConnection]:
|
1321
1559
|
"""Return SSH connection(s) for accessing the host as a target from the controller."""
|
1322
1560
|
settings = SshConnectionDetail(
|
@@ -1334,6 +1572,11 @@ class PosixSshProfile(SshTargetHostProfile[PosixSshConfig], PosixProfile[PosixSs
|
|
1334
1572
|
class WindowsInventoryProfile(SshTargetHostProfile[WindowsInventoryConfig]):
|
1335
1573
|
"""Host profile for a Windows inventory."""
|
1336
1574
|
|
1575
|
+
@property
|
1576
|
+
def name(self) -> str:
|
1577
|
+
"""The name of the host profile."""
|
1578
|
+
return self.config.path
|
1579
|
+
|
1337
1580
|
def get_controller_target_connections(self) -> list[SshConnection]:
|
1338
1581
|
"""Return SSH connection(s) for accessing the host as a target from the controller."""
|
1339
1582
|
inventory = parse_inventory(self.args, self.config.path)
|
@@ -1436,9 +1679,9 @@ def get_config_profile_type_map() -> dict[t.Type[HostConfig], t.Type[HostProfile
|
|
1436
1679
|
def create_host_profile(
|
1437
1680
|
args: EnvironmentConfig,
|
1438
1681
|
config: HostConfig,
|
1439
|
-
controller:
|
1682
|
+
controller: ControllerHostProfile | None,
|
1440
1683
|
) -> HostProfile:
|
1441
1684
|
"""Create and return a host profile from the given host configuration."""
|
1442
1685
|
profile_type = get_config_profile_type_map()[type(config)]
|
1443
|
-
profile = profile_type(args=args, config=config,
|
1686
|
+
profile = profile_type(args=args, config=config, controller=controller)
|
1444
1687
|
return profile
|
@@ -30,6 +30,7 @@ from .host_profiles import (
|
|
30
30
|
SshTargetHostProfile,
|
31
31
|
WindowsInventoryProfile,
|
32
32
|
WindowsRemoteProfile,
|
33
|
+
DebuggableProfile,
|
33
34
|
)
|
34
35
|
|
35
36
|
from .ssh import (
|
@@ -59,6 +60,9 @@ def get_common_variables(target_profile: HostProfile, controller: bool = False)
|
|
59
60
|
# To compensate for this we'll perform a `cd /` before running any commands after `sudo` succeeds.
|
60
61
|
common_variables.update(ansible_sudo_chdir='/')
|
61
62
|
|
63
|
+
if isinstance(target_profile, DebuggableProfile):
|
64
|
+
common_variables.update(target_profile.get_ansiballz_inventory_variables())
|
65
|
+
|
62
66
|
return common_variables
|
63
67
|
|
64
68
|
|
@@ -1,11 +1,15 @@
|
|
1
1
|
"""Test metadata for passing data to delegated tests."""
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
|
+
|
5
|
+
import dataclasses
|
4
6
|
import typing as t
|
5
7
|
|
6
8
|
from .util import (
|
7
9
|
display,
|
8
10
|
generate_name,
|
11
|
+
ANSIBLE_TEST_ROOT,
|
12
|
+
ANSIBLE_LIB_ROOT,
|
9
13
|
)
|
10
14
|
|
11
15
|
from .io import (
|
@@ -22,13 +26,19 @@ from .diff import (
|
|
22
26
|
class Metadata:
|
23
27
|
"""Metadata object for passing data to delegated tests."""
|
24
28
|
|
25
|
-
def __init__(self) -> None:
|
29
|
+
def __init__(self, debugger_flags: DebuggerFlags) -> None:
|
26
30
|
"""Initialize metadata."""
|
27
31
|
self.changes: dict[str, tuple[tuple[int, int], ...]] = {}
|
28
32
|
self.cloud_config: t.Optional[dict[str, dict[str, t.Union[int, str, bool]]]] = None
|
29
33
|
self.change_description: t.Optional[ChangeDescription] = None
|
30
34
|
self.ci_provider: t.Optional[str] = None
|
31
35
|
self.session_id = generate_name()
|
36
|
+
self.ansible_lib_root = ANSIBLE_LIB_ROOT
|
37
|
+
self.ansible_test_root = ANSIBLE_TEST_ROOT
|
38
|
+
self.collection_root: str | None = None
|
39
|
+
self.debugger_flags = debugger_flags
|
40
|
+
self.debugger_settings: DebuggerSettings | None = None
|
41
|
+
self.loaded = False
|
32
42
|
|
33
43
|
def populate_changes(self, diff: t.Optional[list[str]]) -> None:
|
34
44
|
"""Populate the changeset using the given diff."""
|
@@ -55,8 +65,13 @@ class Metadata:
|
|
55
65
|
changes=self.changes,
|
56
66
|
cloud_config=self.cloud_config,
|
57
67
|
ci_provider=self.ci_provider,
|
58
|
-
change_description=self.change_description.to_dict(),
|
68
|
+
change_description=self.change_description.to_dict() if self.change_description else None,
|
59
69
|
session_id=self.session_id,
|
70
|
+
ansible_lib_root=self.ansible_lib_root,
|
71
|
+
ansible_test_root=self.ansible_test_root,
|
72
|
+
collection_root=self.collection_root,
|
73
|
+
debugger_flags=dataclasses.asdict(self.debugger_flags),
|
74
|
+
debugger_settings=dataclasses.asdict(self.debugger_settings) if self.debugger_settings else None,
|
60
75
|
)
|
61
76
|
|
62
77
|
def to_file(self, path: str) -> None:
|
@@ -76,12 +91,20 @@ class Metadata:
|
|
76
91
|
@staticmethod
|
77
92
|
def from_dict(data: dict[str, t.Any]) -> Metadata:
|
78
93
|
"""Return metadata loaded from the specified dictionary."""
|
79
|
-
metadata = Metadata(
|
94
|
+
metadata = Metadata(
|
95
|
+
debugger_flags=DebuggerFlags(**data['debugger_flags']),
|
96
|
+
)
|
97
|
+
|
80
98
|
metadata.changes = data['changes']
|
81
99
|
metadata.cloud_config = data['cloud_config']
|
82
100
|
metadata.ci_provider = data['ci_provider']
|
83
|
-
metadata.change_description = ChangeDescription.from_dict(data['change_description'])
|
101
|
+
metadata.change_description = ChangeDescription.from_dict(data['change_description']) if data['change_description'] else None
|
84
102
|
metadata.session_id = data['session_id']
|
103
|
+
metadata.ansible_lib_root = data['ansible_lib_root']
|
104
|
+
metadata.ansible_test_root = data['ansible_test_root']
|
105
|
+
metadata.collection_root = data['collection_root']
|
106
|
+
metadata.debugger_settings = DebuggerSettings(**data['debugger_settings']) if data['debugger_settings'] else None
|
107
|
+
metadata.loaded = True
|
85
108
|
|
86
109
|
return metadata
|
87
110
|
|
@@ -130,3 +153,70 @@ class ChangeDescription:
|
|
130
153
|
changes.no_integration_paths = data['no_integration_paths']
|
131
154
|
|
132
155
|
return changes
|
156
|
+
|
157
|
+
|
158
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
159
|
+
class DebuggerSettings:
|
160
|
+
"""Settings for remote debugging."""
|
161
|
+
|
162
|
+
module: str | None = None
|
163
|
+
"""
|
164
|
+
The Python module to import.
|
165
|
+
This should be pydevd or a derivative.
|
166
|
+
If not provided it will be auto-detected.
|
167
|
+
"""
|
168
|
+
|
169
|
+
package: str | None = None
|
170
|
+
"""
|
171
|
+
The Python package to install for debugging.
|
172
|
+
If `None` then the package will be auto-detected.
|
173
|
+
If an empty string, then no package will be installed.
|
174
|
+
"""
|
175
|
+
|
176
|
+
settrace: dict[str, object] = dataclasses.field(default_factory=dict)
|
177
|
+
"""
|
178
|
+
Options to pass to the `{module}.settrace` method.
|
179
|
+
Used for running AnsiballZ modules only.
|
180
|
+
The `host` and `port` options will be provided by ansible-test.
|
181
|
+
The `suspend` option defaults to `False`.
|
182
|
+
"""
|
183
|
+
|
184
|
+
args: list[str] = dataclasses.field(default_factory=list)
|
185
|
+
"""
|
186
|
+
Arguments to pass to `pydevd` on the command line.
|
187
|
+
Used for running Ansible CLI programs only.
|
188
|
+
The `--client` and `--port` options will be provided by ansible-test.
|
189
|
+
"""
|
190
|
+
|
191
|
+
port: int = 5678
|
192
|
+
"""
|
193
|
+
The port on the origin host which is listening for incoming connections from pydevd.
|
194
|
+
SSH port forwarding will be automatically configured for non-local hosts to connect to this port as needed.
|
195
|
+
"""
|
196
|
+
|
197
|
+
|
198
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
199
|
+
class DebuggerFlags:
|
200
|
+
"""Flags for enabling specific debugging features."""
|
201
|
+
|
202
|
+
self: bool = False
|
203
|
+
"""Debug ansible-test itself."""
|
204
|
+
|
205
|
+
ansiballz: bool = False
|
206
|
+
"""Debug AnsiballZ modules."""
|
207
|
+
|
208
|
+
cli: bool = False
|
209
|
+
"""Debug Ansible CLI programs other than ansible-test."""
|
210
|
+
|
211
|
+
on_demand: bool = False
|
212
|
+
"""Enable debugging features only when ansible-test is running under a debugger."""
|
213
|
+
|
214
|
+
@property
|
215
|
+
def enable(self) -> bool:
|
216
|
+
"""Return `True` if any debugger feature other than on-demand is enabled."""
|
217
|
+
return any(getattr(self, field.name) for field in dataclasses.fields(self) if field.name != 'on_demand')
|
218
|
+
|
219
|
+
@classmethod
|
220
|
+
def all(cls, enabled: bool) -> t.Self:
|
221
|
+
"""Return a `DebuggerFlags` instance with all flags enabled or disabled."""
|
222
|
+
return cls(**{field.name: enabled for field in dataclasses.fields(cls)})
|